Status: implemented pre-alpha architecture
The server is a small, durable chat system whose first public protocol is IRC. It is not a traditional connection-centric IRC daemon with a bouncer bolted on. IRC clients are adapters onto an account-, device-, conversation-, and event-centric core.
The intended installation is a small community on one standalone server.
Implementation sequencing and the boundaries shared by onboarding, moderation, automation, and external content are maintained in the consolidated implementation plan.
- Account is a durable authenticated identity with an immutable human, bot, or agent kind asserted by the server. Accounts are optional for public servers and public channels.
- Anonymous identity is a pseudonymous session or renewable bearer identity. It does not require an email address, phone number, or stable public host.
- Device is one installation or client credential belonging to an account. Devices have independent delivery checkpoints.
- Connection is a disposable transport session. Reconnecting must not create a new person or silently move another device's cursor.
- Conversation is a channel or direct-message thread.
- Event is an immutable, uniquely identified item in a conversation log. Messages and notices are events; joins, parts, topic changes, edits, and redactions can use the same envelope later.
Internally, every accepted event receives both an opaque global event ID and a
monotonic offset in its conversation. IRC exposes the event ID as msgid; it
does not expose the offset. A small sender-presentation snapshot is stored with
each message so replay shows the nickname and username recipients originally
saw; the stable principal remains authoritative for permissions.
Current avatar and nickname-color presentation is separate account state. It is looked up when a capable client renders a sender, so changing it updates old history without rewriting retained events. Traditional IRC adapters continue to project the stored nickname and message text only.
There are two durable cursors and one transient watermark:
| State | Scope | Advanced by | Meaning |
|---|---|---|---|
| Read marker | account + conversation | an explicit client read action | The furthest event the human is known to have read |
| Delivery checkpoint | account + installation + conversation | an acknowledgement or resumable-session checkpoint | The furthest event this installation can safely resume after |
| Send watermark | connection + conversation | the server writer | The furthest event queued on this socket; never treated as read |
All cursor movements are monotonic. A cursor names an actual event in the same conversation, so a client cannot skip ahead with a timestamp from another room.
Read state is shared across an account because that is the behavior people expect across phone, terminal, and web clients. Delivery state is per installation so one active client cannot make another installation miss messages. Merely writing bytes to TCP never advances either durable cursor.
A durable direct conversation is identified by one unordered pair of stable local account IDs. It is not identified by either participant's current nickname, connection, or installation. Resolving the pair in either direction returns the same conversation and sequence, and every history or cursor operation first derives that pair from the authenticated requester. A message ID from a different pair is rejected as an invalid history anchor.
PRIVMSG Nick remains compatible with ordinary clients. When both endpoints are
authenticated, the server commits the event to their account-pair conversation
and fans it out to all registered connections for both accounts. When either
endpoint is anonymous, delivery is intentionally live-only: the nickname routes
to one current connection and no private history is created. Thus a recycled
nickname can neither identify nor recover another person's retained messages.
NOTICE uses the same channel, live-anonymous, and durable account-pair routes,
but its event type survives storage and replay. The adapter never generates an
automatic error or away response to a notice. This distinction is represented
in event content rather than guessed from presentation text.
The IRC adapter also accepts the experimental @account target for authenticated
offline addressing and stable history/cursor commands. Account names are local
and case-insensitive in this standalone phase. This syntax is documented in
direct messages; it is not presented as an IRCv3 standard.
Modern clients use IRCv3 draft/chathistory, msgid, server-time, and
draft/read-marker. The server synchronizes MARKREAD updates between
connections on the same account.
Clients may also negotiate IRCv3 draft/multiline with its advertised
32-component and 16-KiB limits. Telex validates the complete client batch
before committing one event, reconstructs it before exposing it through the
shared model, and projects readable ordinary lines to clients that do not
support batches. The exact transport, replay, and web-review contract is
documented in multiline messages.
Authenticated human clients can negotiate the experimental, versioned
draft/device-resume-0.2 adapter. After SASL they bind an opaque installation
token that selects only a delivery checkpoint; the token is not a login
credential. Paging advances only after an explicit MARKDELIVERED acknowledgement.
The protocol is documented separately in device resume.
Legacy clients cannot acknowledge delivery or reliably report what is visible. For them, JOIN triggers a configurable bounded overlap of recent retained messages, preferring a possible duplicate over a silent gap. Negotiating chat history disables that fallback. It never advances a cursor, and negotiated message IDs and server timestamps make deduplication and presentation less ambiguous where the client supports them. The complete behavior is documented in legacy client replay. If several clients reuse one saved installation token, they also reuse one delivery checkpoint.
The IRCv3 persistence specs are still marked as drafts, so their wire names and conformance behavior are isolated in the IRC adapter rather than leaking into the domain model.
The account-registration draft is intentionally not implemented yet. Its
current text discourages production deployment, and its REGISTER command has
no field for the invitation bearer token required by private admission.
Overloading email or follow-up VERIFY would be incompatible and would force
the server to retain pending credentials. People instead redeem invitations on
the native onboarding endpoint and then connect with ordinary SASL clients.
Relevant specifications:
- Modern IRC Client Protocol
- IRCv3 chat history
- IRCv3 read markers
- IRCv3 message IDs
- IRCv3 account tags
- IRCv3 account notifications (deferred)
- IRCv3 extended joins
- IRCv3 away notifications
- IRCv3 multi-prefix
- IRCv3 userhost-in-names
- IRCv3 account extbans
- IRCv3 labeled responses
- IRCv3 standard replies
- IRCv3 account registration
- IRCv3 WebSocket transport
SQLite in WAL mode is the default. It is enough for the target scale, keeps deployment to one process, supports transactional cursor checks, and gives operators familiar backup and inspection tools.
Current channel topics live in a channel-owned state table rather than the chat event log, so history paging and installation cursors remain message-oriented. Topic text, a bounded setter presentation snapshot, and its timestamp survive an empty room and process restart. Clearing a topic removes that state. Authorization never depends on the presentation snapshot.
The storage boundary exposes domain operations rather than generic SQL:
- append an event and allocate its conversation offset;
- advance a read marker if the referenced event is newer;
- advance an installation checkpoint if the referenced event is newer;
- page before or after an event ID;
- apply retention without turning a missing old event into a cursor reset.
Retention prunes message bodies but keeps small event-envelope tombstones. This
preserves stable message IDs, conversation ordering, and cursor anchors without
retaining expired content. SQLite secure_delete is enabled before updates so
ordinary database pages are scrubbed. WAL files, filesystem snapshots, and
existing backups require separate checkpoint and lifecycle procedures; logical
retention is not represented as guaranteed forensic erasure.
The agreed policy uses a 90-day server channel default with bounded owner/operator overrides, including live-only and longer finite rooms. The initial private deployment expects its general room to use a shorter override of about 30 days and an annual planning room may use roughly 400 days. Durable account-pair DMs use one operator-configured 180-day server default rather than conflicting participant preferences. Each accepted message captures its expiration so a later policy edit cannot silently lengthen already-posted content. The complete disclosure and change contract is in message retention.
A dedicated bounded worker thread owns the SQLite connection and all runtime writes, so async connection tasks neither block an executor nor share a connection behind a mutex. It resolves case-folded channel names to durable conversation IDs and owns sequence allocation. Reads may use separate SQLite connections later. Message bodies and sensitive metadata never appear in normal logs.
The server runs one plaintext or TLS IRC listener and can bind a separate IRC-over-WebSocket address. Plaintext is restricted to loopback by default; a non-loopback address on either transport requires an explicit development override and suppresses SASL there. TLS configuration applies to both addresses, loads and validates its certificate chain and matching key before binding, bounds incomplete handshakes, and permits password authentication remotely. Certificate hot reload remains planned. Capability negotiation is foundational rather than optional parsing.
The implemented compatibility set is version-aware CAP 301/302 with
cap-notify, SASL PLAIN over TLS or on loopback, message tags, batch, server
time, message IDs, echo message, complete draft chat-history paging and target
discovery, read markers, account tags, extended joins, away
notification, multi-prefix, userhost-in-names, durable type-preserving
NOTICE, durable account-pair direct messages, recipient-owned DM policy with
opaque pre-persistence rejection, and the experimental device-resume adapter.
account-notify is deliberately absent until Telex has a reviewed use for
post-registration account transitions; current sessions authenticate once and
cannot switch accounts.
Labeled command correlation composes zero-, one-, and
multi-line replies without labeling unrelated traffic, while standard replies
preserve established numerics; the exact boundary is in
IRCv3 correlated responses. Identity and presence events
are rendered per recipient, as detailed in
IRCv3 identity and presence. Common
established-client discovery and presence commands are also implemented:
WHO, account-aware WHOX, WHOIS, LIST, AWAY, ISON, USERHOST,
MOTD, LUSERS, and durable TOPIC with 331/332/333 replies. These surfaces
always publish anon.invalid, use the WHOX non-address placeholder rather than
disclosing an IP, hide an invite-only channel from non-members, and do not
support wildcard WHO user enumeration. Exact-nickname WHOIS exposes the
authenticated account through numeric 330, as expected by account-aware IRC
clients, without exposing a device ID. SASL EXTERNAL remains planned.
IRC over WebSocket uses the standardized text.ircv3.net and
binary.ircv3.net subprotocols, preserves client preference, and maps exactly
one unterminated IRC line to each WebSocket message. The handshake and message
limits are bounded. Browser origins are denied unless exactly allowlisted;
native clients without an Origin header remain usable. This makes a browser
client possible without a gateway. The implemented native sync API adds exact
acknowledgements, private control results, and a structured extension boundary
while using the same domain operations and event log rather than becoming a
second chat system. M12 automation and provider-hosted external-content
references now extend that same boundary. Direct DCC negotiation remains live
and connection-scoped; registered XDCC providers receive only ephemeral
transfer-control frames, while clients exchange file bytes outside the daemon.
Deployment details are in IRC over WebSocket.
The full IRC terminal and browser clients share one dependency-free protocol, session, model, and durable-resume coordinator. The native terminal reuses the model and storage contracts through the structured M11 session. A delivery acknowledgement is emitted only after the terminal's flushed atomic file replacement or a committed IndexedDB transaction. The browser requires a persistent-storage grant before enabling resume. Read markers advance from terminal output or actual visible browser scroll content, not merely because a conversation is selected.
- Never disclose a client IP address or reverse DNS name to ordinary users.
- Do not persist raw client IP addresses. Abuse controls use short-lived, rotating keyed hashes when an address-derived key is unavoidable.
- Redact addresses, credentials, tokens, and message bodies from logs.
- Permit anonymous use when the operator enables public access; do not require profile fields or an external identity provider.
- Use TLS for non-loopback client traffic.
- Store each human account password with Argon2id. A password change invalidates every old login, including optional remembered-device bearers, and disconnects live sessions; users intentionally carry one password across clients.
- Give automated deployments random, labeled, independently revocable app keys with only SHA-256 digests retained server-side.
- Authorize retained direct history by stable account pair, never by a current or previously used nickname.
- Make history retention configurable per server and conversation, with a finite privacy-oriented default.
This is not end-to-end encryption. The server operator can read content while the server is running and from retained state or backups. The UI and documentation must say this plainly.
Public connections may remain anonymous. Durable cross-client state requires a local account.
Each human account has a case-insensitive name, no email or profile requirement, and one Argon2id password verifier. IRC clients authenticate with the bare account name through SASL PLAIN over TLS or on loopback. Password verification runs in bounded blocking work with a dummy verification for unknown accounts, and failures do not reveal whether an account exists. Account-wide read markers are synchronized between authenticated connections and survive restart.
An operator can require authentication before connection registration. With
the default operator-only account-registration mode, this is the smallest
authenticated-only server profile. A client that otherwise completed registration receives
FAIL * ACCOUNT_REQUIRED and can still authenticate before the bounded
registration deadline. Configuration fails closed if this policy is combined
with a remote plaintext listener, where password authentication is deliberately
unavailable.
The invitation domain stores only Argon2id hashes of bounded random bearer secrets and atomically creates a human account and password verifier. Issue and revoke capabilities are separate, while an offline infrastructure command exists for bootstrap. No issuer-to-redeemer edge is stored or audited. The implemented native API adds explicit operator/invite registration modes, account-attributed steward actions, bounded request and hashing work, transient keyed source throttles, generic redemption failures, and remote-TLS enforcement. Its same-origin onboarding page loads no external content, uses no session or cookie authority, and clears browser form secrets after use. See invitation core and native control API v1.
Human installation identity is deliberately separate from login authority.
After account-password authentication, a supporting client creates or binds a
random dr1... token stored server-side only as a SHA-256 digest. The token
selects one delivery checkpoint and cannot authenticate. Password changes retain
those checkpoints while invalidating the old verifier and disconnecting live
sessions.
The web client can explicitly opt one installation into a separate random
rd1... remembered-login bearer, also retained server-side only as a digest.
It authenticates the human as account/<installation-uuid> but must still bind
the matching dr1... cursor. It cannot create another bearer or rotate the
account password. Individual revocation preserves the cursor; password change
revokes all remembered bearers. Procedures and trust boundaries are in
remembered devices and account password
changes.
Automated accounts have no account-wide password. Provisioning returns one
server-generated ak1... app key exactly once; its account/key-id
authentication ID names that key. Labeled keys are independently addable and
revocable, and only their SHA-256 digests are stored. This preserves deployment
granularity without making ordinary users manage per-device secrets.
An optional initial-channel subscription is also account state, not connection
state or channel authority. New human accounts receive it atomically at
creation, every authenticated device autojoins it, and a successful PART
deletes it so future connections respect the user's choice. Existing accounts
are never silently backfilled by a configuration change. See initial
channel.
Private channels express membership in terms of stable account IDs, not
nicknames or host masks. They are created private from inception with one
protected owner; existing public channels cannot be relabeled after clients may
have retained their history. Admission carries one revision-consistent durable
policy snapshot into runtime state. JOIN and live policy publication share one
gate, while compare-and-set revisions prevent concurrent writers from silently
overwriting each other. A live private-member removal reconciles all connected
devices and sends standard KICK messages before returning. The CLI
administration path still requires a stopped listener; the running surface is
an infrastructure capability for embedding and future control-plane adapters.
Owner assignment and transfer are explicit atomic operations. Successful grant,
role, owner, account lifecycle, and channel-initialization mutations write
metadata-only records with 30-day retention in the same transaction. Account
disable refuses to strand an owned channel, shares a read/write gate with SASL
and authenticated command dispatch, and disconnects all sessions after the
durable update. Channel
access mode is fixed at creation; there is no unsafe public/private relabel
path. In-band account-backed role, invite, kick, ban, and member-limit checks
use the same revisioned authority snapshot. Details are in
private channels and
authority audit, with IRC presentation in
channel moderation.
Familiar IRC modes are translated at the adapter boundary where possible. The
standard invite-only and ban denials are compatible presentation, not a claim
that transient nickname or host masks are authoritative. The +k
adapter is an optional shared-key condition over that base policy: public +k
admits a server-admitted key holder without creating a durable role, while
+ik requires both the account-backed membership and the key. It never
bypasses server admission, bans, or limits. Argon2 verification is bounded and
performed outside the authority mutation lock, then checked against the same
policy revision under that lock before JOIN commits. The verifier and IRC
disclosure contract are in shared-key channels.
Plain URLs work everywhere immediately. Rich clients may render links and image URLs locally.
Server-fetched previews are absent because they leak reading activity and create an SSRF surface. The IRC daemon does not host uploads or proxy downloads. The implemented structured-reference adapter catalogues files held by an external provider and delivers a short-lived, one-use download capability only to the requester. Legacy IRC clients receive useful URL, filename, size, digest, and warning text. The complete contract is external content references.
The local foundation is complete through account/installation identity, cursor-safe history, IRC/WebSocket adapters, invite onboarding, familiar moderation, DM privacy, configurable retention, content epochs/reset with cooperative reference-client cleanup, the native integration gateway, grant-enforced disclosed automation participation with atomic operating budgets, loop tripwires, cooldown and moderator pause, the narrow MCP adapter with deterministic and model-backed downstream examples, and provider-hosted external-content exchange.
The remaining release work is validation and deployment:
- exercise the documented desktop/mobile/terminal IRC compatibility matrix;
- review the production topology and checked configuration profile;
- complete standalone deployment fault checks and burn-in as an independent acceptance milestone rather than a code gate.
The checkboxes, release boundaries, and accepted decisions live in the roadmap.