Skip to content

Channels — Implementation Plan

Status: Plan only — no implementation started Last updated: 7.apr.2026 Strategy: see channels-roadmap.md

v0.9.0 is in development. Telegram is the only channel and is working. Nothing in this plan should be implemented until v0.9.0 is validated in production.

Two channels for v0.9.0: Telegram (done) + Web UI (new). Signal and WhatsApp deferred until real user demand exists. This covers phone dependency, privacy, and global reach without Java or Meta.


Before writing a single line of channels code, confirm these work on a real install:

  • Telegram bot receives and responds to messages
  • npm run backup produces a valid tarball
  • npm run doctor reports healthy
  • Metrics endpoint responds on port 9100
  • Grafana dashboard shows real data from VictoriaMetrics
  • ZFS snapshots are being taken by Sanoid
  • Management jail starts cleanly on boot

Fix anything broken before proceeding.


Step 1 — Onboarding refactor

Make setup channel-agnostic. Currently assumes Telegram unconditionally.

  • setup/onboarding.ts — no change needed (doesn’t touch Telegram)
  • setup/telegram-auth.ts — wrap in a channel selection step so Telegram is one option, not the only option
  • src/index.ts — guard TELEGRAM_BOT_TOKEN check behind TELEGRAM_ENABLED=true rather than failing hard if token missing
  • .env — add TELEGRAM_ENABLED=true (default true, no breaking change)

Step 2 — Invite code system

New users register by sending a code. Operator generates codes via CLI. Full implementation was prototyped on 16.mar.2026 — see git history or re-derive from channels-plan.md decisions section above.

Files to create:

  • src/invite.tscreateInviteCode, tryConsumeInviteCode, jidToFolder
  • scripts/invite.tsnpm run invite [--ttl Nd] → prints new code
  • scripts/users.tsnpm run users → lists registered users
  • scripts/codes.tsnpm run codes → lists all active/used invite codes
  • scripts/revoke.tsnpm run revoke <jid> → removes a registered user
  • scripts/revoke-code.tsnpm run revoke-code <code> → deletes unused code
  • scripts/suspend.tsnpm run suspend <jid> / npm run unsuspend <jid>

Files to modify:

  • src/db.tsinvite_codes table (with expires_at, revoked_at), invited_by column on registered_groups, suspended_at on registered_groups
  • src/types.tsinvitedBy?: string, suspendedAt?: Date on RegisteredGroup
  • src/channels/telegram.tsonInviteCode? callback in opts, check suspension
  • src/index.ts — wire callback, auto-register on valid code, reject suspended JIDs
  • package.json — add invite, users, codes, revoke, revoke-code, suspend, unsuspend scripts

Key decisions:

  • Single-use codes, optional TTL (default: no expiry)
  • Server stores invite_code → JID mapping permanently (enables re-link on browser clear)
  • Store invited_by from day one for future delegated invites
  • Operator-only invite generation to start (Model A)
  • Audit log: logs/invite-audit.log

Step 3 — Web UI channel

Files to create:

  • src/channels/web.ts — implements Channel, WebSocket server, JID prefix web:
  • Frontend: single HTML/CSS/JS chat page
  • setup/web-ui.ts — deploy frontend to CMS jail nginx

Files to modify:

  • src/index.ts — start web channel on WEB_UI_PORT if WEB_UI_ENABLED=true
  • .envWEB_UI_ENABLED=false, WEB_UI_PORT=3001
  • setup/index.ts — add web-ui step
  • setup/pf.ts — allow/restrict WEB_UI_PORT as appropriate

User identity on Web UI: invite code entered on first visit, stored in localStorage. Same invite system as Step 2 — no new concepts for the user.

Step 4 — Version bump and docs

  • Bump to v0.9.0
  • Update docs/internal/sessions/ with a session log for this work
  • Update install docs to mention Web UI and invite codes

Decision 1 — End user registration model ✓ DECIDED: Invite code

Section titled “Decision 1 — End user registration model ✓ DECIDED: Invite code”

Operator runs npm run invite → gets a short code (e.g. CLAWDIE-A3X7), shares it out of band. User sends it as their first message. Bot registers them automatically.


Decision 2 — Invite code behaviour ✓ DECIDED: Single-use, optional TTL

Section titled “Decision 2 — Invite code behaviour ✓ DECIDED: Single-use, optional TTL”

One code per person, works until used. Store invited_by: jid | 'operator' on every registered user from day one — enables upgrade to delegated invites later without a migration.

Security requirements:

  • Optional TTL: npm run invite --ttl 7d (default: no expiry). TTL stored in invite_codes.expires_at (nullable). Expired codes rejected silently.
  • Audit log: every code creation and consumption appended to logs/invite-audit.log with timestamp, code, JID (on consume), and source.
  • Revoke unused codes: npm run revoke-code <code> hard-deletes an unused code. Used codes cannot be revoked (JID is already registered — use npm run revoke <jid> instead).
  • List codes: npm run codes shows all active codes with status:
CODE CREATED EXPIRES STATUS JID
CLAWDIE-A3X7 2026-03-15 — used web:a3x7
CLAWDIE-B9K2 2026-03-17 2026-03-24 active —

Schema change: add expires_at TIMESTAMP NULL and revoked_at TIMESTAMP NULL to invite_codes table. Rollback migration: DROP COLUMN expires_at; DROP COLUMN revoked_at;


Decision 3 — Web UI user identity ✓ DECIDED: Invite code login

Section titled “Decision 3 — Web UI user identity ✓ DECIDED: Invite code login”

User enters their invite code on first Web UI visit. Code becomes their persistent identity — survives browser clears, works on any device. Reuses the same invite system as Decision 1. No OAuth, no Google dependency.

Identity recovery: localStorage is used for convenience (avoids re-entry on repeat visits) but is NOT the authoritative identity store. The server maintains a permanent invite_code → jid mapping in the invite_codes table. If a user clears browser data, entering the same invite code re-links them to their existing JID — no lockout. tryConsumeInviteCode must check for an existing mapping before creating a new JID:

async function tryConsumeInviteCode(code: string): Promise<{ jid: string; isNew: boolean }> {
const existing = await db.getJidByInviteCode(code);
if (existing) return { jid: existing, isNew: false }; // re-link, not new
// ... create new JID, store mapping
}

signal-cli requires Java. Java in a jail adds attack surface. No mature non-Java Signal client exists. Revisit only when real users ask for it.


Meta-owned, metadata collected, unofficial library only. Revisit only when real users ask for it. If added: use Baileys (pure Node.js, no browser/Puppeteer dependency).


Step 1 — Onboarding refactor (prerequisite for everything)

Section titled “Step 1 — Onboarding refactor (prerequisite for everything)”

What changes:

  • setup/onboarding.ts — replace hardcoded Telegram token prompt with channel selection
  • setup/telegram.ts — Telegram becomes one option, not the default assumption
  • src/db.ts — add invite_codes table, add invited_by field to registered users
  • src/index.ts — handle first-message invite code registration logic
  • npm run invite — new CLI command to generate invite codes
  • npm run users — list registered users with channel and invited_by
  • npm run revoke <jid> — remove a registered user

What does not change:

  • Channel interface — already correct
  • Message pipeline — already channel-agnostic
  • JID storage — already channel-prefixed

What changes:

  • src/channels/web.ts — implements Channel, WebSocket server, JID prefix web:
  • src/index.ts — start web channel on WEB_UI_PORT
  • Frontend: single HTML/CSS/JS chat page served from CMS jail via nginx
  • setup/web-ui.ts — deploy frontend, configure nginx route
  • .envWEB_UI_ENABLED, WEB_UI_PORT
  • Invite code entered on first visit → stored in localStorage for convenience
  • localStorage is not authoritative — server re-links on re-entry (see Decision 3)

Rate limiting (WebSocket):

  • Per-IP: max 10 messages/minute; excess connections dropped with 429
  • Per-JID: suspended users rejected at WebSocket handshake (no session created)
  • Implementation: in-memory Map<ip, { count, resetAt }> in src/channels/web.ts

Operator user management:

  • npm run suspend <jid> — sets suspended_at, blocks all channels for that JID
  • npm run unsuspend <jid> — clears suspended_at, restores access
  • Suspension does not delete data — full history preserved

Access options:

  • Private: Tailscale only (no public exposure)
  • Public: chat.yourdomain.com via nginx, invite code is the only gate

Add only when real users request it. Java/JRE in a jail is the accepted tradeoff at that point.


Add only when real users request it. Tier 2, opt-in, informed-consent warning in setup.


Requires Chinese business registration. Not part of main Clawdie install. JID prefix reserved: wc:


StepTarget versionPriority
1. Onboarding refactorv0.9.0Now — blocks everything else
2. Web UIv0.9.0Now — solves phone dependency
3. SignalFutureOnly on user demand
4. WhatsAppFutureOnly on user demand
5. WeChatCommunityNot in scope