Wire production stack: Clerk + Postgres + Anthropic + per-user containers

Brings the backend from a scaffold to a working end-to-end MVP — real auth,
persistent actor registry, Anthropic tool-use loop in the Grow Agent, and
per-user Gitea+OpenCode provisioning. Also adds the client-facing
architecture diagram under docs/architecture.html.
This commit is contained in:
sai karthik
2026-05-19 22:17:40 +05:30
parent 5eaf52b8a5
commit ff0bf5e5f0
27 changed files with 4599 additions and 358 deletions

181
README.md
View File

@@ -1,101 +1,116 @@
# growqr-backend
# GrowQR — backend + frontend
Backend for the Grow Agent Platform (see [`docs/PRD.md`](docs/PRD.md)).
A multi-agent platform where every user gets a private Grow Agent (Rivet Kit actor) that orchestrates sub-agents and owns a per-user OpenCode + Gitea Docker stack. See `docs/PRD.md` for the product spec.
Per the PRD, every user gets:
## What's wired up
- A **Grow Agent** (Rivet Kit actor) that orchestrates **sub-agent actors**.
- A **dedicated OpenCode Docker** container — every sub-agent workflow executes through it.
- A **dedicated Gitea Docker** container — backs the agent's memory and project repos.
- A frontend (Next.js, separate repo) that pulls the Rivet Kit React SDK and talks to the Grow Agent over the actor connection.
- **Auth**: Clerk (frontend + backend JWT verification).
- **DB**: Postgres + Drizzle (users, actor registry, container mappings, repos, OpenCode sessions, events).
- **Actors**: Rivet Kit — `growAgent` per user (master) and `subAgent` (worker), with a real Anthropic Claude tool-use loop.
- **Per-user containers**: Gitea (memory repo) + OpenCode (workflow execution), spawned via `dockerode`, with admin user + access token bootstrap, ports allocated from a managed pool, lifecycle reconciled on backend boot.
- **Frontend**: Next.js 16 with `@clerk/nextjs` for auth and `rivetkit/client` for direct actor connections + event streaming.
- **Tool surface available to the Grow Agent**: `spawn_sub_agent`, `commit_memory`, `read_memory`, `list_memory`.
## One-time setup
You need three external accounts before running:
1. **Clerk** — create an app at https://dashboard.clerk.com → copy the publishable + secret keys.
2. **Anthropic** — create an API key at https://console.anthropic.com.
3. **Docker** — Docker Desktop (or any daemon `dockerode` can reach via `/var/run/docker.sock`).
Then:
```bash
# Backend env
cp .env.example .env
# fill in CLERK_SECRET_KEY, CLERK_PUBLISHABLE_KEY, ANTHROPIC_API_KEY
# Frontend env
cd growqr-frontend
cp .env.example .env.local
# fill in NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY and CLERK_SECRET_KEY
cd ..
```
## Running
```bash
# 1. Start Postgres + Rivet engine (per-user OpenCode/Gitea containers are
# spawned dynamically by the backend when a user signs in).
docker compose up -d postgres rivet-engine
# 2. Install + migrate
npm install
npm run db:migrate
# 3. Backend
npm run dev # http://localhost:4000
# 4. Frontend (separate terminal)
cd growqr-frontend
npm install
npm run dev # http://localhost:3000
```
Open http://localhost:3000, sign up, verify your email, and the home page will:
1. Mirror your Clerk user into Postgres.
2. Spawn your dedicated Gitea + OpenCode containers (first run pulls images — ~2040s).
3. Connect the Grow Agent chat to your dedicated Rivet actor.
4. Stream agent + sub-agent events back to the UI.
## Architecture
```
Frontend (Next.js + Rivet Kit React SDK)
Backend (Hono + Rivet Kit client + dockerode)
├─▶ rivet-engine (compose service) → Grow Agent + Sub-Agent actors
└─▶ Docker daemon (host)
├─ growqr-gitea-<userId> (one per user, spawned on demand)
└─ growqr-opencode-<userId> (one per user, spawned on demand)
Browser
Clerk JWT
Next.js (3000) ──fetch──▶ /users/bootstrap, /users/me (Hono on 4000)
└─Rivet client──▶ /api/rivet/* (Hono → registry.handler)
Grow Agent actor (one per user)
├─ Anthropic (Opus 4.7 + tool use)
├─ commit_memory ────▶ Gitea container (per user)
└─ spawn_sub_agent ────▶ OpenCode container (per user)
(multiplexed sessions)
```
The backend mounts the host Docker socket so it can spawn the per-user
container pair via `dockerode` on `POST /actors/provision`.
## Layout
```
src/
config.ts env config
log.ts pino logger
index.ts Hono app entrypoint
docker/manager.ts dockerode wrapper — spawns Gitea + OpenCode per user
actors/
grow-agent.ts Rivet Kit master actor (one per user)
sub-agent.ts Rivet Kit worker actor (workflows → OpenCode Docker)
registry.ts Rivet Kit actor registry setup
routes/
actors.ts PRD §5.2 — actor registry HTTP API
opencode.ts PRD §5.3 — OpenCode Docker management
git.ts PRD §5.4 — Gitea Docker management
```
## Local setup
Prereqs: Node 22+, Docker, `docker compose`.
## Useful commands
```bash
cp .env.example .env
npm install
docker compose up -d rivet-engine # start Rivet engine
npm run dev # start backend on :4000
npm run typecheck # backend
npm run db:generate # diff schema → new migration
npm run db:studio # browse Postgres via Drizzle Studio
cd growqr-frontend
npx tsc --noEmit # frontend types
npm run lint
```
The backend pulls the Gitea + OpenCode images on first user provision (no need to pre-pull).
## Troubleshooting
## Smoke test
- **"missing bearer token"** from `/users/bootstrap` — Clerk session not attached. Sign out and back in.
- **`Gitea did not become ready`** during provisioning — Gitea takes 1020s on first pull. Wait, then `POST /actors/provision` (the frontend retries via polling).
- **OpenCode container exits immediately** — check `OPENCODE_IMAGE`. The compose env passes `Cmd: ["serve", ...]`; if you swap to a different image, ensure it exposes the `opencode serve` HTTP surface on `:4096`.
- **`No free ports in USER_PORT_RANGE`** — bump `USER_PORT_RANGE_END` in `.env` or stop unused user stacks via `POST /actors/stop`.
```bash
# Provision a Grow Agent + spawn its OpenCode + Gitea Docker
curl -X POST localhost:4000/actors/provision \
-H 'content-type: application/json' \
-d '{"userId":"u_alice"}'
## PRD status
# Send a message
curl -X POST localhost:4000/actors/u_alice/message \
-H 'content-type: application/json' \
-d '{"text":"hello"}'
All MVP items in `docs/PRD.md` §9 are implemented:
# Spawn a sub-agent
curl -X POST localhost:4000/actors/u_alice/sub-agents \
-H 'content-type: application/json' \
-d '{"type":"coding","channelId":"ch-coding-1"}'
- [x] User auth (Clerk)
- [x] Actor registry (Postgres + Rivet)
- [x] One Grow Agent Rivet Kit actor per user
- [x] Sub-agent registration under the Grow Agent
- [x] Per-user Gitea Docker + memory repo
- [x] Memory commits into the user's Gitea
- [x] Per-user OpenCode Docker + session API
- [x] Message endpoint from frontend to Grow Agent (via Rivet)
- [x] Event stream from agent to frontend (via Rivet broadcast)
- [x] Frontend: login, master chat, sub-agent progress, channel-style logs
# Run a workflow through OpenCode Docker
curl -X POST localhost:4000/actors/u_alice/sub-agents/coding-<id>/run \
-H 'content-type: application/json' \
-d '{"prompt":"scaffold a Hello World"}'
# Stop the user's stack
curl -X POST localhost:4000/actors/u_alice/stop
```
`docker ps` should show `growqr-gitea-u_alice` and `growqr-opencode-u_alice` after provision.
## What's stubbed
These are wired structurally but not yet talking to real upstream APIs:
- OpenCode HTTP/SSE session API (the `routes/opencode.ts` forwarder is a stub).
- Gitea repo/commit calls (the `routes/git.ts` handlers are stubs).
- Auth (PRD §5.1) and payments (PRD §5.5).
- Persistent registry — currently in-memory; replace with a real DB.
## Frontend
The Next.js frontend lives at `../growqr-frontend` (currently cloned as a nested dir at `./growqr-frontend` — git-ignored from this repo). It will install `@rivetkit/react` and connect to the Grow Agent actor for chat + streaming events.
Payments and the full quest/pathway runner are deferred to v2 (PRD §10).