new file mode 100644
index 0000000..b705a25
@@ -0,0 +1,125 @@
+---
+visibility: public
+---
+
+# Architecture
+
+## One-paragraph summary
+
+A chat (1:1 or group) arrives via Linq webhook. picortex looks up or provisions a Unix user + home dir + tmux session for that chat's stable ID. The inbound message is appended to the canonical SQLite log, then — after attention gating — injected into the chat's tmux session via `send-keys`, wrapped in turn sentinels. Claude Code's response is parsed out of the tmux pipe-pane log and sent back via Linq. A mobile-first web UI attaches to the same tmux session through an xterm.js WebSocket bridge and displays a browseable file tree of the chat's home.
+
+## Component diagram
+
+```
+┌────────────────────────────────────────────────────────────────────────┐
+│ Linq (or linq-sim) │
+└────────────┬───────────────────────────────────────┬───────────────────┘
+ │ HMAC-signed webhook │ /api/partner/v3/*
+ ▼ ▲
+ ┌───────────────────────────────────────────────────┴────┐
+ │ picortex backend (Fastify, TS, pino) │
+ │ │
+ │ Channel<Linq> ─▶ Router ─▶ AttentionGate ─▶ │
+ │ │ │ │
+ │ ▼ ▼ │
+ │ SQLite log TurnDispatcher
+ │ │ │ │
+ │ │ ▼ │
+ │ │ runuser -u chat-X │
+ │ │ │ │
+ │ ▼ ▼ │
+ │ ┌─────────────────────────────────┐ │
+ │ │ tmux picortex:<chat_id> │ │
+ │ │ └─ claude (Claude Code) │ │
+ │ │ └─ pipe-pane → session.log │ │
+ │ └─────────────────────────────────┘ │
+ │ ▲ ▲ │
+ │ │ │ │
+ │ ReplyCapture ────────┘ │ │
+ │ │ │ │
+ │ ▼ │ │
+ │ Channel<Linq>.send │ │
+ │ │ │
+ │ /ws/terminal/:chat_id ───────────────┘ node-pty │
+ │ │
+ └─────────────────────────────────────────────────────────┘
+ ▲ ▲
+ │ HTTP + WS │ HTTP (files API)
+ │ │
+ ┌───────────────────────────────────────────────────┐ ┌──────┐
+ │ Frontend (Vite + React, :7824) │ │ ... │
+ │ swipe-panels: messages │ files │ terminal │ └──────┘
+ └───────────────────────────────────────────────────┘
+```
+
+## Invariants
+
+- **I1 Canonical log:** the SQLite `messages` table is authoritative. The workspace FS is a cache.
+- **I2 Per-chat user:** no chat's bytes ever reach another chat's process, enforced by POSIX perms.
+- **I3 Sentinel protocol:** every Claude turn is bracketed in tmux by unique start/end sentinels so reply capture is unambiguous.
+- **I4 Backend-as-root:** the backend can enter any chat user via sudoers; chat users have no sudo.
+- **I5 Channel interface:** business logic does not know about Linq, only about the `Channel` interface.
+- **I6 HMAC on inbound:** no unsigned webhook is processed. Full stop.
+
+## Data model (SQLite)
+
+```sql
+CREATE TABLE chats (
+ id TEXT PRIMARY KEY, -- from Linq
+ kind TEXT NOT NULL, -- '1on1' | 'group'
+ display_name TEXT,
+ owner_phone TEXT NOT NULL,
+ unix_user TEXT UNIQUE NOT NULL,
+ home_dir TEXT NOT NULL,
+ created_at INTEGER NOT NULL,
+ last_message_at INTEGER
+);
+
+CREATE TABLE messages (
+ id TEXT PRIMARY KEY,
+ chat_id TEXT NOT NULL REFERENCES chats(id),
+ direction TEXT NOT NULL, -- 'inbound' | 'outbound'
+ author TEXT, -- phone or bot
+ text TEXT,
+ reply_to_message_id TEXT,
+ turn_id TEXT, -- present on outbound, ties to tmux turn
+ request_id TEXT NOT NULL,
+ raw_event JSON,
+ created_at INTEGER NOT NULL
+);
+
+CREATE TABLE events (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ kind TEXT NOT NULL,
+ chat_id TEXT,
+ request_id TEXT,
+ payload JSON,
+ created_at INTEGER NOT NULL
+);
+
+CREATE TABLE bridge_events (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ src_chat_id TEXT NOT NULL,
+ dst_chat_id TEXT NOT NULL,
+ path TEXT NOT NULL,
+ sha256 TEXT NOT NULL,
+ approver_phone TEXT NOT NULL,
+ challenge_message_id TEXT NOT NULL,
+ approval_message_id TEXT NOT NULL,
+ created_at INTEGER NOT NULL
+);
+
+CREATE TABLE chat_config (
+ chat_id TEXT PRIMARY KEY REFERENCES chats(id),
+ attention_mode TEXT NOT NULL DEFAULT 'mentions-only',
+ discriminator_model TEXT,
+ discriminator_threshold REAL,
+ updated_at INTEGER NOT NULL
+);
+```
+
+## Runtime
+
+- **picortex service:** systemd unit `picortex.service`, starts Fastify + Vite-built static frontend.
+- **tmux per chat:** spawned on demand under the chat user.
+- **cron:** `picortex-lifecycle.timer` runs hourly for idle reap + archive.
\ No newline at end of file