@@ -341,60 +341,117 @@ for anything else. There is no nested `error` object — a client that reads `ms
341341
342342### Receiving Events
343343
344- When subscribed data changes, server pushes events:
344+ When subscribed data changes, the server pushes ` event ` messages.
345+
346+ ** ` EventMessageSchema ` declares five fields of its own** — ` subscriptionId ` , ` eventName ` , ` object ` ,
347+ ` payload ` and ` userId ` — on top of the three ` BaseWebSocketMessage ` fields every message carries.
348+ They are camelCase, and the record body travels ** inside ` payload ` ** , never at the top level.
345349
346350** Created Event:**
347351``` json
348352{
353+ "messageId" : " a41d6c58-92b7-4f0e-8d3a-16b5c7e04912" ,
349354 "type" : " event" ,
350- "subscription_id" : " sub_1" ,
351- "event" : " created" ,
355+ "timestamp" : " 2024-01-16T14:30:00Z" ,
356+ "subscriptionId" : " 9f8c1e42-5b7a-4d33-9e10-6c2f8a91b4d7" ,
357+ "eventName" : " data.record.created" ,
352358 "object" : " task" ,
353- "data" : {
354- "id" : " task_456" ,
355- "title" : " New task assigned to you" ,
356- "status" : " todo" ,
357- "assignee_id" : " user_123" ,
358- "created_at" : " 2024-01-16T14:30:00Z"
359+ "userId" : " user_123" ,
360+ "payload" : {
361+ "id" : " 0c8b7a36-4e21-4d59-b7f2-3a9e5d1c8046" ,
362+ "type" : " data.record.created" ,
363+ "object" : " task" ,
364+ "recordId" : " task_456" ,
365+ "after" : {
366+ "id" : " task_456" ,
367+ "title" : " New task assigned to you" ,
368+ "status" : " todo" ,
369+ "assignee_id" : " user_123" ,
370+ "created_at" : " 2024-01-16T14:30:00Z"
371+ },
372+ "userId" : " user_123" ,
373+ "timestamp" : " 2024-01-16T14:30:00Z"
359374 }
360375}
361376```
362377
363378** Updated Event:**
364379``` json
365380{
381+ "messageId" : " b7e2f409-51ac-4d68-9b30-2c7a8e15d643" ,
366382 "type" : " event" ,
367- "subscription_id" : " sub_1" ,
368- "event" : " updated" ,
383+ "timestamp" : " 2024-01-16T15:00:00Z" ,
384+ "subscriptionId" : " 9f8c1e42-5b7a-4d33-9e10-6c2f8a91b4d7" ,
385+ "eventName" : " data.record.updated" ,
369386 "object" : " task" ,
370- "data" : {
371- "id" : " task_456" ,
372- "status" : " in_progress" ,
373- "updated_at" : " 2024-01-16T15:00:00Z"
374- },
375- "changes" : {
376- "status" : {
377- "old" : " todo" ,
378- "new" : " in_progress"
379- }
387+ "userId" : " user_123" ,
388+ "payload" : {
389+ "id" : " 5d3c9f71-8a04-4b62-91e7-0f6b2d8a3c15" ,
390+ "type" : " data.record.updated" ,
391+ "object" : " task" ,
392+ "recordId" : " task_456" ,
393+ "changes" : {
394+ "status" : " in_progress"
395+ },
396+ "after" : {
397+ "id" : " task_456" ,
398+ "status" : " in_progress" ,
399+ "updated_at" : " 2024-01-16T15:00:00Z"
400+ },
401+ "userId" : " user_123" ,
402+ "timestamp" : " 2024-01-16T15:00:00Z"
380403 }
381404}
382405```
383406
384407** Deleted Event:**
385408``` json
386409{
410+ "messageId" : " c9a04b17-6d38-4e52-8f71-4b2e6c9d0537" ,
387411 "type" : " event" ,
388- "subscription_id" : " sub_1" ,
389- "event" : " deleted" ,
412+ "timestamp" : " 2024-01-16T16:00:00Z" ,
413+ "subscriptionId" : " 9f8c1e42-5b7a-4d33-9e10-6c2f8a91b4d7" ,
414+ "eventName" : " data.record.deleted" ,
390415 "object" : " task" ,
391- "data" : {
392- "id" : " task_456" ,
393- "deleted_at" : " 2024-01-16T16:00:00Z"
416+ "userId" : " user_123" ,
417+ "payload" : {
418+ "id" : " e18f2b05-7c46-4a93-b2d0-9e35c7a14f68" ,
419+ "type" : " data.record.deleted" ,
420+ "object" : " task" ,
421+ "recordId" : " task_456" ,
422+ "userId" : " user_123" ,
423+ "timestamp" : " 2024-01-16T16:00:00Z"
394424 }
395425}
396426```
397427
428+ <Callout type = " warn" >
429+ ⛔ ** ` subscription_id ` , ` event ` and ` data ` are not fields of this envelope, and ` changes ` and
430+ ` reason ` are not top-level fields either.** The declared names are ` subscriptionId ` , ` eventName `
431+ and ` payload ` , so a client written from the old fences reads ` msg.subscription_id ` and ` msg.data `
432+ and gets ` undefined ` on both. ` changes ` is real but sits ** one level down** , on the ` DataEvent `
433+ inside ` payload ` , and it carries the write's own patch — a flat ` { field: newValue } ` map
434+ (` publishDataEvent ` , ` packages/objectql/src/engine.ts ` ), not the ` old ` / ` new ` pairs this page
435+ used to show; read the post-state from ` payload.after ` . ` reason ` is declared nowhere in the
436+ protocol at all — see ** Subscribe to Query Results** below.
437+ </Callout >
438+
439+ ** Fields:**
440+ - ` messageId ` : UUID for this message (` BaseWebSocketMessage ` )
441+ - ` timestamp ` : ISO 8601 datetime the message was sent (` BaseWebSocketMessage ` )
442+ - ` subscriptionId ` : ** UUID** of the subscription this event belongs to — the schema declares
443+ ` z.string().uuid() ` , and it is the same UUID the subscribe message declared, so an opaque handle
444+ like ` "sub_1" ` does not parse
445+ - ` eventName ` : the event name, dot notation by convention. The platform-checked vocabulary is the
446+ closed ` DataEventType ` / ` BulkDataEventType ` enums (` packages/spec/src/api/events.zod.ts ` ) —
447+ ` data.record.created ` / ` data.record.updated ` / ` data.record.deleted ` , plus the two bulk siblings
448+ - ` object ` : optional object name the event relates to — the one key the old fences had right
449+ - ` payload ` : the event body, declared ` z.unknown() ` on the envelope. The platform's only publisher
450+ today is the ObjectQL engine, which emits a ` DataEvent ` for a single-row write and a
451+ ` BulkDataEvent ` for a predicate write; that is the shape shown above
452+ - ` userId ` : optional — the user who triggered the event
453+
454+
398455### Unsubscribe
399456
400457Stop receiving events for a subscription:
@@ -492,37 +549,55 @@ the declared subscription carries no query:
492549- Evaluate query membership ** on the client** : apply the predicate to each event and derive the
493550 enter/exit transitions yourself by comparing against the set you are already holding
494551
495- ** Example:** Task enters subscription:
552+ ** Example:** a ` task ` the subscription covers is updated so that it now matches your predicate :
496553``` json
497554{
555+ "messageId" : " d2c74f38-9b15-4e06-a7f3-8c1d5b902e47" ,
498556 "type" : " event" ,
499- "subscription_id" : " sub_3" ,
500- "event" : " updated" ,
557+ "timestamp" : " 2024-01-16T14:46:00Z" ,
558+ "subscriptionId" : " 6b3c9e07-2d54-4f18-9c8a-0e7d1b5a4632" ,
559+ "eventName" : " data.record.updated" ,
501560 "object" : " task" ,
502- "data" : {
503- "id" : " task_789" ,
504- "status" : " todo" , // Changed from "in_progress" to "todo"
505- "assignee_id" : " user_123"
506- },
507- "reason" : " entered_query"
561+ "payload" : {
562+ "id" : " 7a1e4c92-0d63-4f85-b91a-2e6c8d357b04" ,
563+ "type" : " data.record.updated" ,
564+ "object" : " task" ,
565+ "recordId" : " task_789" ,
566+ "changes" : { "status" : " todo" },
567+ "after" : { "id" : " task_789" , "status" : " todo" , "assignee_id" : " user_123" },
568+ "timestamp" : " 2024-01-16T14:46:00Z"
569+ }
508570}
509571```
510572
511- ** Example:** Task exits subscription:
573+ ** Example:** the same subscription, a ` task ` updated so that it no longer matches :
512574``` json
513575{
576+ "messageId" : " e6f18a03-4d27-4c91-8b52-7a0e3d9c1465" ,
514577 "type" : " event" ,
515- "subscription_id" : " sub_3" ,
516- "event" : " updated" ,
578+ "timestamp" : " 2024-01-16T14:47:00Z" ,
579+ "subscriptionId" : " 6b3c9e07-2d54-4f18-9c8a-0e7d1b5a4632" ,
580+ "eventName" : " data.record.updated" ,
517581 "object" : " task" ,
518- "data" : {
519- "id" : " task_456" ,
520- "status" : " done" // No longer matches "todo" filter
521- },
522- "reason" : " exited_query"
582+ "payload" : {
583+ "id" : " 9c05b7d4-1a68-4e30-97f2-5d8a1c60e3b9" ,
584+ "type" : " data.record.updated" ,
585+ "object" : " task" ,
586+ "recordId" : " task_456" ,
587+ "changes" : { "status" : " done" },
588+ "after" : { "id" : " task_456" , "status" : " done" },
589+ "timestamp" : " 2024-01-16T14:47:00Z"
590+ }
523591}
524592```
525593
594+ ⭐ ** The two messages are identical in shape — nothing in the envelope says "entered" or "exited".**
595+ ` EventMessageSchema ` has no ` reason ` field, and no runtime tracks query membership to populate one,
596+ so the earlier ` reason: "entered_query" ` / ` "exited_query" ` examples described a signal the contract
597+ cannot send. The transition is yours to derive: apply your predicate to ` payload.after ` and compare
598+ against the set you are already holding.
599+
600+
526601## Server-Sent Events (SSE)
527602
528603### Connection Endpoint
@@ -731,20 +806,34 @@ Real-time subscriptions respect object-level and row-level permissions:
731806
732807** Example:** User has rule "see only assigned tasks":
733808
734- ``` javascript
735- // User is assigned task_456
809+ ``` json
736810{
811+ "messageId" : " f3b90d67-5a24-4e18-9c07-1b8f2a6d4e35" ,
737812 "type" : " event" ,
738- " event" : " updated" ,
813+ "timestamp" : " 2024-01-16T14:51:00Z" ,
814+ "subscriptionId" : " 9f8c1e42-5b7a-4d33-9e10-6c2f8a91b4d7" ,
815+ "eventName" : " data.record.updated" ,
739816 "object" : " task" ,
740- " data" : { " id" : " task_456" , " assignee_id" : " user_123" },
741- " reason" : " entered_query"
817+ "userId" : " user_123" ,
818+ "payload" : {
819+ "id" : " 2d7c1f85-6b39-4a04-8e13-9f05c2b71a68" ,
820+ "type" : " data.record.updated" ,
821+ "object" : " task" ,
822+ "recordId" : " task_456" ,
823+ "changes" : { "assignee_id" : " user_123" },
824+ "after" : { "id" : " task_456" , "assignee_id" : " user_123" },
825+ "userId" : " user_123" ,
826+ "timestamp" : " 2024-01-16T14:51:00Z"
827+ }
742828}
743-
744- // Task reassigned to someone else
745- // No event sent - task is now invisible to user
746829```
747830
831+ Reassign ` task_456 ` to someone else and, under the row-level behaviour described above, no further
832+ message arrives for it: the protocol has no "you lost access" signal to send, so absence of events
833+ is the whole signal. There is no ` reason ` field here either. What delivery actually enforces today
834+ is the implementation-status callout at the top of this page, not this section.
835+
836+
748837## Scaling Considerations
749838
750839### Connection Limits
0 commit comments