Skip to content
setu

a portfolio piece by Amar Gupta

A bridge between your browser and the Claude Code session running on your desktop.

Setu relays chat from the web, mobile, WhatsApp, and Telegram into the same Claude Code agent — through one Supabase project, two MCP servers, and a channel-based wake protocol that paints replies live across every surface.

<1s
wake latency, web → desktop
5 + 4
MCP tools, Vercel + local
1
Supabase project, RLS-scoped
4
channels: web · mobile · WA · TG

Flow

One message, four hops, sub-second.

No webhooks, no polling, no cron. Channel-based wakes ride Supabase Realtime end-to-end so the desktop Claude session reacts the instant the browser composer hits Enter.

  1. 01

    Browser tab

    POST /api/chat/send writes the user row, broadcasts on user:<uid>.

    ~80ms

  2. 02

    Supabase Realtime

    Wake frame fans out to every subscribed surface (web, mobile, desktop).

    ~120ms

  3. 03

    Sutra (Electron)

    SSE bridge drops the frame onto the local MCP at 127.0.0.1:9000.

    ~40ms

  4. 04

    Claude Code

    chat_respond_* streams partial assistant tokens straight back through pgvector.

    live

System map

Four surfaces, one agent.

Drawn as it runs — not a Mermaid box-and-arrow. Each lane is a real Node process or a real channel; each hairline is a real network hop with measured latency.

Surfaces

user-facing

Browser

next 16 · rsc

iOS / Android

expo · fcm push

WhatsApp

baileys · cloud api

Telegram

multi-bot · webhook

Cloud

vercel + supabase

POST /api/chat/send

api

route handler · runtime nodejs

chat_messages

db

rls · replica identity full · pgvector

Supabase Realtime

bus

broadcasts on user:<uid>

/api/mcp (OAuth)

api

rfc 8707 · pkce s256 · dcr

Wake protocol

Inserting a user row triggers a Realtime broadcast on a channel keyed by user:<auth.uid()>. Every subscribed surface paints it instantly — no polling, no webhooks, no cron.

Desktop

electron host

Sutra (Electron 33)

sse bridge · ipc · keytar

Local MCP

127.0.0.1:9000 · stateless

Claude Code

chat_respond_* tools

Replies stream back

UPDATE chat_messages SET status = 'streaming' → Supabase fans the partial token diff back through Realtime → browser repaints in < 60ms.

p50 wake

180ms

browser send → desktop CC frame received

p99 paint

760ms

tap composer → first streaming token painted

push fan-out

fire-and-forget

expo + fcm dispatched after the write, never blocks

How it's built

Four decisions that earned their keep.

Each is in production and each has a debug-session story behind it — the kind of detail recruiters skim past on a resume.

Case study 01

Twin MCP servers, one schema

Local desktop CC needs sub-millisecond access to the chat tools, but external clients on other machines still need to attach over the internet with proper auth.

5 + 4tools Vercel + Sutra
  1. Split the surface in two: a Streamable HTTP MCP at /api/mcp on Vercel (OAuth + RFC 8707 audience-bound bearers) and a stateless local MCP at 127.0.0.1:9000 inside the Electron host.

  2. Every chat-write tool is implemented twice — the handler code is duplicated, but the schema and Supabase project are shared so RLS is the single source of truth.

  3. A non-negotiable line in the repo guide reminds future-me to fix bugs in both copies. The CI lint enforces export const runtime = "nodejs" on every API route so node:crypto + jose imports do not crash the edge bundle.

excerpt · 01.code
// app/api/mcp/route.ts
export const runtime = 'nodejs'

const server = createMcpServer({
  tools: [chat_send, chat_respond,
          chat_respond_begin, chat_respond_chunk, chat_respond_finish],
  authenticate: bearerDispatch, // pm_* | oat_* | raw JWT
})

export const POST = streamableHttp(server, { enableJsonResponse: true })

Case study 02

Streaming Postgres into every surface

Browser, mobile, WhatsApp and Telegram all want to paint the assistant's reply as it streams — without polling and without a custom websocket server.

< 60mspaint of partial token
  1. chat_messages carries a status column (streaming → final) and a partial-content column. The publication has REPLICA IDENTITY FULL so UPDATE events carry the full row, not just changed columns.

  2. Browsers subscribe to postgres_changes AND to a per-thread broadcast channel. Subscribe-before-load + lastSeenCreatedAt resync closes the race-condition gap that fired when a wake landed mid-page-load.

  3. Mobile clients consume the same Realtime stream over an exponential-backoff WS reconnect; FCM/Expo push fires AFTER the row write so the response path never blocks on Google.

excerpt · 02.code
// supabase migration 025
alter publication supabase_realtime
  add table chat_messages;

alter table chat_messages
  replica identity full;

-- now: UPDATE events carry the full row, including
-- partial streaming content, so browser repaints
-- without a second SELECT roundtrip.

Case study 03

Channel-based wakes, not polling

The desktop Claude Code instance is idle 99% of the time. Polling every second would burn a Claude session API quota; webhooks couldn't reach a NAT'd laptop. We needed a push-from-the-cloud primitive.

0cron, webhooks, or polling
  1. Each user owns a Realtime channel named user:<auth.uid()>. The Vercel API broadcasts the wake frame the instant the user-row is inserted.

  2. Sutra (Electron) maintains a long-lived SSE bridge to that channel and relays the frame into the local MCP at 127.0.0.1:9000 — the Claude CLI, launched with --dangerously-load-development-channels server:setu-channel, reacts to a notifications/claude/channel JSONRPC frame.

  3. Routine wakes (scheduled from pg_cron) ride the same primitive on a dedicated swayam-channel so code-edits and routine fires never share an attention head.

excerpt · 03.code
// sutra/src/main/mcp/sse-stream.ts
const channel = supabase
  .channel(`user:${userId}`)
  .on('broadcast', { event: 'chat_message' }, ({ payload }) => {
    relayToLocalMcp({
      method: 'notifications/claude/channel',
      params: { content: payload.content, meta: payload.meta },
    })
  })
  .subscribe()

Case study 04

OAuth broker as a route handler

External Claude Code clients (a recruiter's laptop, a CI runner) need to attach via OAuth without paying for a third-party identity provider, and the implementation has to survive a security audit.

RFC 8707audience-bound tokens
  1. PKCE S256 + Dynamic Client Registration + RFC 8707 audience-bound access tokens implemented as Next.js route handlers — no external Auth0 / Clerk dependency.

  2. Refresh-token rotation tracks the issuance chain (rotated_to column). The endpoint will walk the chain and revoke the entire descendant tree the instant a revoked token is replayed.

  3. Short-lived (5 min TTL) HS256 JWTs propagate the user id AND an app_actor claim ("web" | "mcp" | "sync" | "admin") read by the audit-log trigger inside Postgres — so every row written carries provenance back to the API surface that wrote it.

excerpt · 04.code
// app/oauth/token/route.ts
export const runtime = 'nodejs'

export async function POST(req: Request) {
  const grant = await parseGrant(req)
  if (grant.type === 'authorization_code') {
    verifyPkceS256(grant.code, grant.code_verifier)
  }
  if (grant.type === 'refresh_token') {
    const { row, replayed } = await findRefreshToken(grant.refresh_token)
    if (replayed) await revokeChainFrom(row) // RFC 6749 §10.4
    await revokeAndRotateRefreshToken(row)
  }
  return issueShortJwt(grant.user_id, 'mcp')
}

What this proves

Six skills, one running system.

Setu is dogfooded daily. Every layer here is in production traffic — not a sandbox, not a screenshot.

01 · agent infra

MCP server design

Two parallel servers — OAuth-protected /api/mcp on Vercel for external clients, and a local 127.0.0.1:9000 inside an Electron relay. Idempotency hashing, audience-bound bearer dispatch, RFC 8707 PKCE + DCR.

02 · database

Streaming Postgres

pgvector + Supabase Realtime with REPLICA IDENTITY FULL on chat_messages so browsers paint partial assistant tokens via UPDATE events alongside INSERTs. RLS on every row, JWT actor propagated via app_actor claim.

03 · distribution

Cross-platform delivery

Same Claude session reachable from web, iOS / Android push (Expo + FCM dual-provider), WhatsApp Cloud + Baileys self-chat, Telegram multi-bot with AES-256-GCM token storage. Per-channel dispatch path, fire-and-forget push.

04 · desktop

Desktop relay (Electron)

Sutra hosts the local MCP, an SSE bridge to Supabase Realtime, a browser-bridge for Claude-in-Browser Act dispatch, a WebRTC capture host, encrypted keytar storage, and a custom sutra:// deep-link scheme. All wired with strict TS + Node 20.

05 · frontend

Next.js 16 + App Router discipline

No middleware (per-layout auth gating for explicit data flow), Server Components by default, host-only cookies, Vercel deploys, OAuth broker as route handlers, Streamable HTTP MCP transport.

06 · security

Production telemetry

Audit-log trigger on every write with redacted embedding columns, two-step token revocation, OAuth refresh-token rotation chain, latent-bug ledger in /docs with 29 findings anchored to file:line.

MCP surface

Nine tools, two hosts, one bridge.

Five chat tools live on Vercel as a Streamable HTTP MCP for external Claude Code clients. Four relay tools live in Sutra at 127.0.0.1:9000 so the desktop session never round-trips through the cloud.

Vercel · /api/mcp

5 tools
host
setu.devfrend.com/api/mcp
transport
Streamable HTTP · JSON-RPC
auth
OAuth 2.1 · PKCE S256 · RFC 8707
  • chat_sendmutation

    Inserts user row in chat_messages, broadcasts user:<uid>.

  • chat_respondmutation

    Single-shot final reply (1-sentence ack only).

  • chat_respond_beginmutation

    Start streaming reply, returns message_id seed.

  • chat_respond_chunkmutation

    Append delta to a streaming assistant row.

  • chat_respond_finishmutation

    Flip status final, optional content overwrite.

Sutra · loopback

4 tools
host
127.0.0.1:9000/setu/api/mcp
transport
HTTP · stateless per request
auth
cookie session · same-origin only
  • browser_open_claude_panelrpc

    Open Claude-in-Browser side panel on a target tab.

  • browser_dispatch_to_panelrpc

    Send an Act prompt to the open Claude panel.

  • scheduled_routine_createmutation

    Create a cron-driven routine.

  • scheduled_routine_listread

    List the operator's routines.

Open to work

I built every layer of this — I can build yours.

Setu is one of five dogfooded apps I ship in parallel. Same MCP fabric, same Supabase project, same Claude Code stack. Happy to walk a hiring panel through any layer live — protocol, schema, relay, push, or the agent itself.

Senior / staff full-stackAI engineer · MCP & agent infraNext.js / TypeScript freelanceRAG + Postgres + pgvector