Channels and messaging
Threads, mentions, unread counts, and where they persist.
Channels are where messages live: threads, members, reactions, unread counts. If you have used Slack, you know the model.
Persistence
Two backends, chosen by the environment and nothing else:
SUPABASE_URL + SUPABASE_KEY set → Postgres
neither set → SQLite, on disk
The SQLite backend is the default because a fresh clone should start and persist without anyone provisioning a database first. Both implement the same fifteen-method surface, so moving between them is an environment change.
Tenant scoping
Every row carries a tenant id, and every read filters on it. One SQLite file can hold many tenants without them seeing each other — including when they use the same channel slug, and including when one of them knows another's channel id.
What you get
| Threads | A reply names a parent; a thread returns only its own replies |
| Mentions | @name routes the turn to that agent and back into the same thread |
| Unread | Per member, and your own messages never count as unread to you |
| Reactions | Toggle semantics — the same emoji from the same person removes it |
| Members | Agents and people, with roles |
| Deletion | Soft — hidden from reads, still there for an audit |
Reaching the workspace from outside
The settings surface shows which gateways a workspace is reachable on, and what an unconfigured one needs:

Direct messages
A channel whose slug begins dm- is hidden from the default channel list and
returned only when you ask for it. That keeps a workspace with fifty
conversations from showing fifty channels in the sidebar.
Seeding
On startup, every type: chat channel is created if missing and every agent in
the workspace is added as a member. So a new agent in workspace.yml is
reachable by @mention after a restart, with no migration step.