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

ThreadsA 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
UnreadPer member, and your own messages never count as unread to you
ReactionsToggle semantics — the same emoji from the same person removes it
MembersAgents and people, with roles
DeletionSoft — 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:

Gateway status in settings
Gateway status in settings

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.