Add Vercel config with React Router preset, vercel-build script and /api/auth rewrite to the Convex site. Add VDS staging compose for the Rivet engine and runner plus systemd units, deployment plan, and Docker/Vercel ignore rules. Lockfile covers both the web and agents dependency additions.
15 KiB
Deployment Architecture Recommendation
Status: Proposed — research completed 2026-07-31. This supersedes the shared-Convex-environment approach in
docs/deployment.mdonce implemented.Goal: retain an instant local iteration loop while operating one stable public staging environment with reproducible, declarative infrastructure.
Decision
Use a deliberately split stack:
Fast iteration (private development) Stable staging (public)
──────────────────────────────────── ────────────────────────
Mac + Tailscale Vercel + Contabo VDS
Web / Flue / Rivet / runner Web SSR Agents VDS
│ │ │
personal Convex dev deployment staging Convex deployment
| Concern | Iteration | Staging |
|---|---|---|
| User-visible URL | Existing Tailscale URL on the Mac | https://staging.<domain> on Vercel |
| Product backend | Personal Convex dev deployment | Dedicated long-lived Convex deployment |
| Web | Vite HMR on the Mac | Vercel React Router SSR |
| Agent worker | Local Flue, engine, and runner | Contabo VDS: Flue, Rivet Engine, AgentOS runner |
| Delivery | Run local pnpm dev:tailscale |
CI applies IaC then updates immutable images |
| State/credentials | Development-only | Independent staging secrets, OAuth app, and volumes |
Do not create a second remote “fast iteration” stack. It duplicates the slowest part of the loop—building and replacing container images—while the existing Mac/Tailscale stack already exercises the complete topology from a phone. Staging is the public link and integration gate; local development is the fast environment.
Why this split
The repository has three materially different runtime needs:
- Web is a React Router SSR Node application. Its current production artifact is
react-router buildplusreact-router-serve, andapps/web/Dockerfilealready serves it on Node. Vercel supports React Router SSR and streaming, and is the appropriate managed host for this stateless application. Vercel React Router guide - Convex owns authentication, durable product data, workflows, and reactive client projections. It is not an application container to run on the VDS. The architecture explicitly requires clients to communicate only with Convex. (
docs/TECH.md, §1.) - Flue + Rivet Engine + AgentOS runner are persistent worker processes. The runner creates Git worktrees, installs dependencies, and host-mounts those directories into AgentOS; they require a long-lived filesystem and must stay co-located with their workspace volume. They are not appropriate for Vercel or Cloudflare Workers. Flue Node Docker deployment · Rivet runtime modes
Cloudflare remains valuable for authoritative DNS, TLS/DDoS controls, and optionally a later public-edge layer. It is not the first frontend runtime choice: moving the current Node SSR artifact to Workers requires a Cloudflare-specific React Router/workerd build and nodejs_compat; it does not host the private worker stack. Cloudflare React Router guide
Environment isolation — required, not optional
The present shared Convex deployment is incompatible with two simultaneously operating environments. SITE_URL is a single deployment-scoped Better Auth base/trusted origin and GitHub OAuth callback origin; switching it between hosts breaks the other host (docs/deployment.md, lines 19–47).
Create these independent Convex deployments:
| Deployment | Type | Purpose |
|---|---|---|
dev:<developer> |
Convex dev | Local loop only; each developer owns one |
staging |
Long-lived production-type deployment | Public integration/staging; never reused for local testing |
| Branch previews | Ephemeral preview deployment | Optional later, only for frontend/backend changes that do not need OAuth |
Convex supports a named long-lived production-type deployment (convex deployment create staging --type prod), per-deployment environment variables, and branch preview deployments. Convex multiple deployments · Convex environment variables
Every environment gets its own:
SITE_URL, exact browser origin;FLUE_URL/AGENT_BACKEND_URL, pointing at only that environment’s Flue worker;FLUE_DB_TOKENshared only with that environment’s agent service;- model and Git provider credentials;
- GitHub OAuth application/client credentials where GitHub login is enabled.
A GitHub OAuth App permits one callback URL. Use a staging OAuth App with https://staging.<domain>/api/auth/callback/github; keep local development on its own OAuth app or login mechanism. Do not promise OAuth on throwaway Vercel preview domains. GitHub OAuth Apps
Staging topology
flowchart LR
Browser[Browser] -->|HTTPS| Vercel[Vercel: staging web SSR]
Browser -->|queries & mutations| Convex[Convex: staging]
Vercel -->|/api/auth rewrite| ConvexSite[Convex Site auth routes]
Convex -->|service-authenticated HTTPS| Flue[Contabo: Flue Node service]
Flue --> Engine[Rivet Engine: private Docker network]
Runner[AgentOS runner: private Docker network] --> Engine
Runner --> Volumes[Persistent source mirror + workspaces]
CF[Cloudflare DNS] --> Vercel
CF --> Flue
Public surface
staging.<domain>→ Vercel.agents-staging.<domain>→ Contabo Caddy/Traefik → Flue only.- Only ports 80/443 (and Tailscale/managed SSH) enter the VDS. Do not publish Rivet ports
6420or6421, the AgentOS runner, workspace directories, or any engine dashboard. - The Flue route is reachable from Convex, but its middleware must continue to require the per-environment bearer
FLUE_DB_TOKENand organization/turn correlation headers. This is a private worker protocol exposed narrowly for Convex callbacks—not a browser API. - Browser clients continue to talk only to Convex. Do not restore browser-to-Flue traffic or a generic
/api/*agent proxy.
Private VDS services
The checked-in Compose topology should contain exactly these services:
| Service | Network exposure | Persistent data | Notes |
|---|---|---|---|
engine |
Internal only | /data bind mount |
Single-node RocksDB is suitable for staging. Set an admin token; probe :6420/health. |
agents |
Caddy upstream only | Shared workspace bind mount | Runs the existing Flue Node build and owns worktree preparation; /health is a liveness endpoint. |
runner |
Internal only | Shared workspace bind mount | Separate from agents; needs Bun, Git, and the same worktree paths prepared by agents. |
caddy |
80/443 only | Caddy certificate/config bind mounts | TLS for the Flue callback hostname. |
Rivet’s filesystem backend is explicitly appropriate for single-node deployments; multi-node/HA later requires PostgreSQL and NATS. Configure the engine with a persistent /data bind mount, admin token, resource limits, and a :6420/health probe. Rivet Docker Compose · Rivet production checklist
The runner must receive a build-time RIVET_RUNNER_VERSION derived from the CI run or immutable release revision. Rivet uses it to route new actors to the new runner and drain old actors; without it, existing actors can continue on old code.
IaC model: Pulumi + Ansible + Compose, each at the right seam
Dokploy is intentionally excluded: it has already proved too slow and imperative for this stack. One tool should not be forced to manage three different concerns poorly.
| Layer | Source of truth | Tool | Reason |
|---|---|---|---|
| SaaS control plane | infra/pulumi |
Pulumi TypeScript | Declarative Cloudflare DNS and Vercel project/domain/environment configuration; one staging stack now, later production; encrypted stack secrets and preview/refresh drift workflows. |
| VDS baseline | infra/ansible |
Ansible | Idempotent OS convergence: service user, Docker, firewall, Tailscale, directories, Caddy prerequisites, and backup timer. No Pulumi SSH-command pseudo-provider. |
| VDS application topology | deploy/compose |
Docker Compose | Explicit services, networks, volumes, healthchecks, images, and restart policy in the repository. This is the deployable unit. |
| Release execution | CI | Gitea Actions or an equivalent CI runner | Builds tagged images, pushes them to a registry, runs ansible-playbook, then applies a pinned Compose release and checks health. |
Pulumi state is meaningful only with a shared backend—use Pulumi Cloud or a managed/self-hosted state backend, not a developer-local file:// state file. Pulumi’s state is what enables previews, refreshes, encrypted secret tracking, and drift detection. Pulumi state and backends · Pulumi secrets
Ansible is not redundant: it makes the Contabo host reproducible without pretending that SSH command resources are declarative infrastructure. Ansible basic concepts
Delivery workflow
Local iteration
- Keep using the current root
pnpm dev:tailscalestack and personal Convex dev deployment. - Use the Mac’s Tailscale URL for phone testing.
- Never edit staging Convex variables, staging OAuth callback settings, or staging VDS volumes from the local loop.
- Merge only when targeted runtime smoke testing and repository checks pass.
Staging release
- A merge to the staging branch (initially
masterif that is the stable branch) starts CI. - CI runs type checks and targeted tests.
- CI builds the web for the staging Convex deployment and deploys it to the dedicated staging Vercel project. Build-time
VITE_CONVEX_URLandVITE_AUTH_URLmust be staging values. - CI builds immutable
agentsandrunnerimages tagged with commit SHA; it setsRIVET_RUNNER_VERSIONfrom the release identity. - CI runs Ansible convergence, updates the Compose release to those exact image digests, and executes
docker compose up -d. - CI verifies: Vercel URL returns 200, Convex auth origin is accepted, agents health endpoint returns 200, engine health returns 200 inside the private network, and a real signed-in staging conversation receives an agent response.
- Rollback means redeploying the previous image digests and restoring the corresponding Vercel deployment—not rebuilding mutable
latestimages.
Do not couple the Vercel deployment to Gitea-native Git integration assumptions. The repository is hosted on Gitea, so start with CI invoking the Vercel CLI/API. If a Git mirror is later introduced, Vercel previews can be enabled separately.
Required implementation backlog
This research does not deploy anything. Before the first staging release, complete these changes in order:
- Create the separate
stagingConvex deployment and its deploy key; configure deployment-scoped secrets andSITE_URL. - Correct the auth/webhook origin seams:
- Add Vercel production rewrites for
/api/auth/*to the staging Convex Site URL; Vite’s current proxy only applies to local development. - Make the Puter webhook target the staging Convex Site HTTP action directly, rather than the web origin.
- Add Vercel production rewrites for
- Add Vercel React Router support and a dedicated staging deployment configuration.
- Create the four-service Compose topology and a runner-capable image. The current agents Dockerfile starts only Flue and does not provide the dedicated Bun/Git runner service.
- Add health routes/checks, resource limits, volume backup, image-digest deployments, and
RIVET_RUNNER_VERSION. - Add
infra/pulumiandinfra/ansible, then CI environments with protected staging secrets. - Execute an end-to-end staging smoke: sign in, create/connect a project, send a conversation, and complete a disposable issue-to-PR job.
Security and operations guardrails
- Keep environment secrets in CI environment secret stores, Pulumi encrypted configuration/ESC where appropriate, Convex deployment variables, and Vercel environment variables. Never commit
.envfiles or place secrets in image layers. - Use unique
FLUE_DB_TOKEN, Rivet admin token, workspace token, model credential, and Git token per environment. - The VDS runs code-writing agents. Use a dedicated service user; do not mount the host home directory; expose only scoped repository credentials to individual attempts; and keep staging separate from any production host. This follows the repository-isolation policy in
docs/TECH.md§11. - Back up Rivet engine state and Caddy configuration. Treat workspaces as reproducible/ephemeral unless an active job requires retention; prune completed worktrees deliberately.
- A single Flue Node instance is the correct initial staging shape. Its durable Convex adapter survives restarts, but each conversation still requires one live owner—do not add replicas until ownership routing is designed. Flue database guide
Sources
- Repository architecture:
docs/TECH.md§§1, 10–11;docs/LOCAL_SETUP.md;apps/web/Dockerfile;packages/agents/Dockerfile;packages/agents/src/runtime/repository-workspace.ts;packages/agents/src/runtime/attempt-runner.ts. - Convex environments and deployments, hosting on Vercel, and HTTP actions.
- Vercel React Router and Vercel environments.
- Rivet self-hosted Docker Compose, production checklist, and version upgrades.
- Flue Node/Docker deployment and durable database ownership.
- Pulumi state, Pulumi secrets, and Ansible.