User Notifications
This example shows how to model in-app notification state in Causet — tracking creation, read status, and unread counts.
Design
Notifications are created by external consumers that subscribe to domain events (e.g., a service that listens to ARTIST_FOLLOWED on Kafka and creates a notification for the followed artist’s fans). Causet tracks the notification state.
Events
# events/notification.events.causet
events:
NOTIFICATION_CREATED:
state: user
entity_expr: event.user_id
payload:
notification_id: string
user_id: string
notification_type: string # not "type" — reserved
title: string
body: string
reference_id: string
NOTIFICATION_READ:
state: user
entity_expr: event.user_id
payload:
notification_id: string
user_id: string
NOTIFICATION_DELETED:
state: user
entity_expr: event.user_id
payload:
notification_id: string
user_id: stringActions
# actions/notification.actions.causet
actions:
CREATE_NOTIFICATION:
state: user
input:
notification_id: { type: string, required: true }
user_id: { type: string, required: true }
notification_type: { type: string, required: true }
title: { type: string, required: true }
body: { type: string, required: true }
reference_id: { type: string, required: false }
side_effects:
rules:
- name: emit_created
then:
- op: emit
event_type: NOTIFICATION_CREATED
payload:
notification_id: intent.notification_id
user_id: intent.user_id
notification_type: intent.notification_type
title: intent.title
body: intent.body
reference_id: intent.reference_id
MARK_NOTIFICATION_READ:
state: user
input:
notification_id: { type: string, required: true }
user_id: { type: string, required: true }
side_effects:
rules:
- name: emit_read
then:
- op: emit
event_type: NOTIFICATION_READ
payload:
notification_id: intent.notification_id
user_id: intent.user_id
DELETE_NOTIFICATION:
state: user
input:
notification_id: { type: string, required: true }
user_id: { type: string, required: true }
side_effects:
rules:
- name: emit_deleted
then:
- op: emit
event_type: NOTIFICATION_DELETED
payload:
notification_id: intent.notification_id
user_id: intent.user_idProjection
# projections/notification.projections.causet
projections:
user_notifications:
source_events: [NOTIFICATION_CREATED, NOTIFICATION_READ, NOTIFICATION_DELETED]
target:
table: user_notifications
primary_key: [notification_id]
fields:
notification_id: TEXT
user_id: TEXT
notification_type: TEXT
title: TEXT
body: TEXT
reference_id: TEXT
read: BOOLEAN
deleted: BOOLEAN
created_at: BIGINT
updated_at: BIGINT
derive:
notification_id: event.notification_id
user_id: event.user_id
updated_at: event.ts
mutations:
NOTIFICATION_CREATED: { op: upsert }
NOTIFICATION_READ: { op: upsert }
NOTIFICATION_DELETED: { op: upsert }
indexes:
- columns: [user_id]
- columns: [user_id, read, deleted]Queries
# queries/notification.queries.causet
queries:
notifications_for_user:
from: user_notifications
input:
user_id: { type: string, required: true }
where:
user_id: { eq: input.user_id }
deleted: { eq: false }
order_by:
created_at: desc
limit: 50
unread_notification_count:
from: user_notifications
input:
user_id: { type: string, required: true }
where:
user_id: { eq: input.user_id }
read: { eq: false }
deleted: { eq: false }
group_by:
- user_id
aggregate:
unread_count: { count: notification_id }
limit: 1Note: The
unread_notification_countquery uses thegroup_by+aggregatepattern. Afields: - count: notification_idapproach is silently dropped by the compiler. Always use theaggregate:block with a matchinggroup_by.
External integration pattern
External services create notifications by submitting intents:
// External "notification service" — NOT inside Causet rules
// Subscribes to causet.ledger-events.v1 Kafka topic
async function handleArtistFollowed(event: LedgerEvent) {
if (event.event_type !== 'ARTIST_FOLLOWED') return;
const artistFollowers = await getArtistFollowers(event.payload.artist_id);
for (const follower of artistFollowers) {
await submitCausetIntent({
action: 'CREATE_NOTIFICATION',
entityId: follower.user_id,
forkId: 'production',
payload: {
notification_id: `notif_${event.entity_id}_${follower.user_id}`,
user_id: follower.user_id,
notification_type: 'artist_followed',
title: 'New follower',
body: `${event.payload.user_id} followed ${event.payload.artist_id}`,
reference_id: event.payload.artist_id,
}
});
}
}This pattern:
- Keeps Causet rules deterministic (no external API calls)
- Allows the notification service to evolve independently
- Makes notification creation an explicit, auditable intent