Skip to content

Commit 3213661

Browse files
authored
Merge pull request #2 from Incultnitollc/feat/hosted-relay
feat: hosted relay core (cloud mode, sub-project #1)
2 parents dd3093d + 109852d commit 3213661

28 files changed

Lines changed: 2396 additions & 7 deletions

docs/superpowers/plans/2026-06-21-cc-bridge-hosted-relay-core.md

Lines changed: 1352 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
# cc-bridge Hosted Relay — Design Spec
2+
3+
**Date:** 2026-06-21
4+
**Status:** Approved (design), pending implementation plan
5+
**Scope of this doc:** Sub-project #1 of 3 — the cross-machine relay core.
6+
7+
## Context
8+
9+
`cc-bridge` v0.1.0 is a local, single-machine JSONL message bridge between Claude
10+
Code sessions (MIT, shipped to npm + global bin). `send` appends to a per-room
11+
JSONL file; `listen` tails it. Everything lives on one machine's disk.
12+
13+
The goal is to **sell cc-bridge as a hosted relay SaaS** (recurring revenue). A
14+
local single-machine MIT tool has no moat — it is trivially copyable. The moat is
15+
a thing customers can't clone: rooms that live in *our* authed database, syncing
16+
sessions across **different machines** (and later, teammates), behind a paywall.
17+
18+
This is three sub-projects. Each gets its own spec → plan → build:
19+
20+
| # | Sub-project | Delivers |
21+
|---|---|---|
22+
| **1 (this doc)** | **Cross-machine relay core** | One account's sessions sync in real time across machines + history |
23+
| 2 | Billing / entitlement | Stripe subscription gates relay access |
24+
| 3 | Distribution | `/b` command + `cc-bridge login` onboarding + pricing page |
25+
26+
2 and 3 are meaningless without 1, so 1 is designed and built first.
27+
28+
## Decisions locked (from brainstorm)
29+
30+
- **Relay model:** real-time (both sessions live), latency sub-second — fine for hand-offs, not an interrupt of a working agent.
31+
- **Backend:** Supabase open-core. Postgres + Realtime + Auth + RLS in one managed service. **No custom server to run.**
32+
- **Auth:** Supabase email OTP (6-digit code), CLI-friendly.
33+
- **Default backend:** logged in → cloud (the point of logging in); `--local` / `CC_BRIDGE_LOCAL=1` forces today's JSONL files.
34+
- **Open-core:** local JSONL mode stays MIT/free and untouched.
35+
36+
## Architecture
37+
38+
```
39+
A's machine: cc-bridge send → INSERT into messages (room, body) ┐
40+
├─ Supabase
41+
B's laptop: cc-bridge listen → SUBSCRIBE realtime where room = X ◀─────┘
42+
(RLS: owner = auth.uid() — you only ever see your own rooms)
43+
```
44+
45+
The existing CLI already routes all storage through exactly two functions —
46+
`sendMessage()` and `listen()`. The code above them (`src/bin/cc-bridge.ts`) is
47+
transport-agnostic: it consumes a `ListenController { iterator, close }` and
48+
prints. Cloud mode is therefore a **backend swap of two functions**, not a
49+
rewrite. One new dependency, `@supabase/supabase-js`, covers auth + insert +
50+
realtime.
51+
52+
A dedicated **new** Supabase project hosts this (isolation + clean billing later).
53+
Free tier.
54+
55+
- **Account:** incultnitopeng@gmail.com
56+
- **Project name:** cc-bridge
57+
- **Project ref:** lyktygwrmhfdxqoqzdxb
58+
- **Project URL:** https://lyktygwrmhfdxqoqzdxb.supabase.co
59+
- **Anon key:** _pending from user (Settings → API → anon public)._ Plugged into `CC_BRIDGE_SUPABASE_URL` + `CC_BRIDGE_SUPABASE_ANON_KEY` (env override) with baked defaults in `src/lib/config.ts`.
60+
61+
## Data model — one table, one migration
62+
63+
```sql
64+
create table messages (
65+
id text primary key, -- reuse existing ULID
66+
v int not null, -- schema version (reuse SCHEMA_VERSION)
67+
ts timestamptz not null,
68+
room text not null,
69+
sender text not null, -- "from" (reserved word → sender)
70+
recipient text, -- "to"
71+
reply_to text,
72+
kind text not null default 'text',-- text | event
73+
msg text not null,
74+
owner uuid not null default auth.uid(),
75+
created_at timestamptz not null default now()
76+
);
77+
78+
create index on messages (owner, room, id); -- replay + per-room queries
79+
80+
alter table messages enable row level security;
81+
create policy own_select on messages for select using (owner = auth.uid());
82+
create policy own_insert on messages for insert with check (owner = auth.uid());
83+
84+
-- Realtime, RLS-aware
85+
alter publication supabase_realtime add table messages;
86+
```
87+
88+
Rooms stay implicit (a string column). No `rooms` table — YAGNI.
89+
90+
Column-name mapping in the CLI: `from → sender`, `to → recipient`. The wire
91+
`Message` shape (validated by the existing zod schema) is unchanged; mapping
92+
happens only at the DB boundary.
93+
94+
## Auth — Supabase email OTP
95+
96+
```
97+
cc-bridge login you@email.com → Supabase emails a 6-digit code
98+
→ paste code → tokens cached at
99+
~/.cc-bridge/credentials (mode 0600)
100+
cc-bridge whoami → prints account email
101+
cc-bridge logout → clears creds
102+
```
103+
104+
The cached **user JWT** is used for both the REST insert and the Realtime
105+
subscribe, so RLS isolation is automatic — no custom auth server. The refresh
106+
token keeps the session alive across CLI invocations.
107+
108+
Alternative considered and deferred: dashboard-generated API keys. More to build
109+
(a web dashboard, a keys table, custom JWT minting). OTP needs none of that.
110+
111+
## CLI seam
112+
113+
New dependency: `@supabase/supabase-js`.
114+
115+
- **`sendRemote(input)`** — calls `buildMessage()` (reused, unchanged) to produce
116+
the validated `Message`, then INSERTs the mapped row. Returns the same `Message`
117+
the local path returns.
118+
- **`listenRemote(opts)`** — subscribes to `postgres_changes` INSERT on `messages`
119+
filtered by `room=eq.X`, yields the **same `ListenController { iterator, close }`**
120+
the bin already consumes. Bin code unchanged.
121+
- **Backend switch** — a small resolver: logged in and not `--local`/`CC_BRIDGE_LOCAL`
122+
→ cloud; else → existing JSONL path. Both `send` and `listen` consult it.
123+
124+
New commands: `login`, `logout`, `whoami`. Existing `send` / `listen` / `rooms` /
125+
`validate` keep their flags.
126+
127+
## Realtime + replay (the one fiddly bit)
128+
129+
`--replay N` and reconnect risk dropping or duplicating messages in the gap
130+
between "query backlog" and "subscription starts". Correct order:
131+
132+
1. Subscribe to the room's realtime channel; buffer live inserts.
133+
2. Query the backlog (last N by `id` — ULIDs sort by time).
134+
3. Dedupe buffered live inserts against backlog by `id`.
135+
4. Drain backlog, then live buffer, then steady-state.
136+
137+
ULID `id`s make ordering and dedupe trivial (no separate sequence needed).
138+
139+
## Security (trust boundaries — not simplified away)
140+
141+
- **Isolation** via RLS `owner = auth.uid()` on select and insert. A customer can
142+
never read or write another account's rooms. This is the core trust boundary
143+
and is mandatory even in the free beta.
144+
- Credentials file `~/.cc-bridge/credentials` written mode `0600`.
145+
- The anon/public Supabase key is shippable in the CLI (it is public by design);
146+
all access is gated by the user JWT + RLS, not by key secrecy.
147+
- Message size cap (existing 64 KB schema limit) carries over; enforced by zod
148+
before insert.
149+
150+
## Open-core boundary
151+
152+
| Mode | Storage | License | Cost to user |
153+
|---|---|---|---|
154+
| Local (today) | per-room JSONL files | MIT, free | free |
155+
| Cloud (this spec) | Supabase, authed | hosted service | free in beta; paid in #2 |
156+
157+
Local mode is not modified. Cloud mode is additive.
158+
159+
## Deliberate ceilings (`ponytail:`)
160+
161+
- **One account across many machines** is the MVP. Teammates = a `team_members`
162+
table + an RLS tweak (`owner in (my teams' owners)`); that is a separate spec,
163+
built only after one-account works.
164+
- **No billing gate yet** → any authenticated user can use the relay (free private
165+
beta). The `owner`/RLS structure already leaves the seam for #2's `subscribed`
166+
check (a `profiles.subscribed` boolean folded into the RLS policy).
167+
- **Realtime latency** is sub-second, not instant. Accepted ceiling.
168+
- If Supabase Realtime ever caps out, the transport can be swapped behind the same
169+
two-function seam without touching the CLI surface.
170+
171+
## Success criteria
172+
173+
1. `cc-bridge login` on two different machines with the same email → both authed.
174+
2. `cc-bridge send "hi" --room demo` on machine A appears in
175+
`cc-bridge listen --room demo` on machine B within ~1s.
176+
3. `cc-bridge listen --room demo --replay 10` prints the last 10 messages then
177+
tails live, with no duplicates across the replay/live boundary.
178+
4. A second account cannot see account #1's `demo` room (RLS verified).
179+
5. `--local` / `CC_BRIDGE_LOCAL=1` still uses JSONL files, behavior identical to
180+
v0.1.0.
181+
182+
## Deferred to later specs
183+
184+
- **#2 Billing:** Stripe subscription, webhook → `profiles.subscribed`, RLS gate.
185+
- **#3 Distribution:** `/b` slash command (folder-derived room name at the command
186+
layer), `cc-bridge login` onboarding flow, pricing page.
187+
- Teams / shared rooms across multiple accounts.

package-lock.json

Lines changed: 95 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"prepublishOnly": "npm run typecheck && npm run lint && npm run test && npm run build"
5353
},
5454
"dependencies": {
55+
"@supabase/supabase-js": "^2.108.2",
5556
"ansi-regex": "^6.1.0",
5657
"chokidar": "^4.0.3",
5758
"commander": "^12.1.0",

0 commit comments

Comments
 (0)