channel / reference
Schema
channelDomain declares one canonical message entity for every communication channel, four internal runtime entities, and two links into the events domain. Provider-specific records (a Resend email, a Twilio message) stay in app/provider domains and link back to channel_messages via externalId/raw.channelDomain
import { channelDomain } from "@ekairos/channel/schema";
// domain("channel").includes(eventsDomain).withSchema({ entities, links, rooms })The domain is named channel and composes eventsDomain, so it carries event_contexts and event_items with it — that is what the links below attach to.
channel_messages
The canonical message crossing any channel. This is the one public entity: your UI queries it, your endpoints write to it.
| prop | type | description |
|---|---|---|
| channel * | string — indexed | Channel kind: web, email, whatsapp, or any custom kind. |
| direction * | string — indexed | inbound or outbound, relative to your system. |
| role | string — optional, indexed | Conversational role: user, assistant, or system. |
| text | string — optional | Plain-text body of the message. |
| parts | json — optional | Structured message parts (rich content, attachments, tool output). |
| status | string — optional, indexed | Delivery status: pending, sending, sent, delivered, read, failed, or a provider-specific value. |
| externalId | string — optional, indexed | Provider-side id (Twilio SID, email message-id, ...). |
| participant | string — optional, indexed | Resolved identity of the counterpart (phone, email address, user id). |
| raw | json — optional | The original provider payload, kept verbatim for audit and debugging. |
| createdAt * | date — indexed | Creation timestamp; the timeline sort key. |
| updatedAt | date — optional | Last update timestamp (e.g. on status transitions). |
Internal runtime entities
The remaining four entities are internal runtime state for platform delivery (subscriptions, locks, caches, queues). They are owned by the channel domain so the whole channel stack persists on InstantDB — consumers never touch these entities directly.
channel_state: i.entity({
key: i.string().unique().indexed(),
value: i.json().optional(),
expiresAt: i.date().optional().indexed(),
updatedAt: i.date(),
}),| prop | type | description |
|---|---|---|
| key * | string — unique, indexed | State entry key. |
| value | json — optional | Arbitrary state payload. |
| expiresAt | date — optional, indexed | TTL for cache-style entries. |
| updatedAt * | date | Last write timestamp. |
channel_locks: i.entity({
threadId: i.string().unique().indexed(),
token: i.string(),
expiresAt: i.date().indexed(),
}),| prop | type | description |
|---|---|---|
| threadId * | string — unique, indexed | One lock per platform thread. |
| token * | string | Lock ownership token. |
| expiresAt * | date — indexed | Lock expiry — stale locks are reclaimed. |
channel_subscriptions: i.entity({
threadId: i.string().unique().indexed(),
createdAt: i.date(),
}),| prop | type | description |
|---|---|---|
| threadId * | string — unique, indexed | Subscribed platform thread. |
| createdAt * | date | When the subscription was created. |
channel_queues: i.entity({
threadId: i.string().indexed(),
seq: i.number().indexed(),
entry: i.json(),
createdAt: i.date(),
}),| prop | type | description |
|---|---|---|
| threadId * | string — indexed | Thread the queued entry belongs to. |
| seq * | number — indexed | Ordering sequence within the thread. |
| entry * | json | The queued payload. |
| createdAt * | date | Enqueue timestamp. |
channel_state, channel_locks, channel_subscriptions and channel_queues as private to the delivery runtime. Query and write channel_messages only.Links
Two links attach canonical messages to the events domain.
channel_messagesContext: {
forward: { on: "channel_messages", has: "one", label: "context" },
reverse: { on: "event_contexts", has: "many", label: "channelMessages" },
},
channel_messagesItem: {
forward: { on: "channel_messages", has: "one", label: "item" },
reverse: { on: "event_items", has: "many", label: "channelMessages" },
},| prop | type | description |
|---|---|---|
| channel_messagesContext | channel_messages.context ⇄ event_contexts.channelMessages | Each message belongs to one context; a context has many messages. This is the link the timeline queries traverse with where: { "context.id": contextId }. |
| channel_messagesItem | channel_messages.item ⇄ event_items.channelMessages | Optionally anchors a message to one context item (the event that produced or consumed it); an item has many messages. Anchored messages render inside their event in the thread timeline. |
Composition
channelDomain includes eventsDomain; the agent domain in turn includes channelDomain. Pushing the agent domain therefore gives you the whole stack — events, channel, and agent entities — in one InstantDB app.
import { agentDomain } from "@ekairos/agent/schema";
export default agentDomain.toInstantSchema();Only using channel without the agent layer? Push channelDomain.toInstantSchema() instead. Have your own app domain? Compose it:
import { domain } from "@ekairos/domain";
import { agentDomain } from "@ekairos/agent/schema";
import { i } from "@instantdb/core";
const appDomain = domain("app")
.includes(agentDomain)
.withSchema({
entities: {
orders: i.entity({
number: i.string().unique().indexed(),
total: i.number(),
}),
},
links: {},
rooms: {},
});
export default appDomain.toInstantSchema();npx instant-cli@latest push schema