Files
zopu-code/docs/DEPLOYMENT_PLAN.md
-Puter 08bc5ae259 feat(web): vercel deployment and vds compose/systemd setup
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.
2026-08-01 20:18:09 +05:30

15 KiB
Raw Blame History

Deployment Architecture Recommendation

Status: Proposed — research completed 2026-07-31. This supersedes the shared-Convex-environment approach in docs/deployment.md once 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:

  1. Web is a React Router SSR Node application. Its current production artifact is react-router build plus react-router-serve, and apps/web/Dockerfile already 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
  2. 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.)
  3. 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 1947).

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 environments Flue worker;
  • FLUE_DB_TOKEN shared only with that environments 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 6420 or 6421, 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_TOKEN and 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.

Rivets 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. Pulumis 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

  1. Keep using the current root pnpm dev:tailscale stack and personal Convex dev deployment.
  2. Use the Macs Tailscale URL for phone testing.
  3. Never edit staging Convex variables, staging OAuth callback settings, or staging VDS volumes from the local loop.
  4. Merge only when targeted runtime smoke testing and repository checks pass.

Staging release

  1. A merge to the staging branch (initially master if that is the stable branch) starts CI.
  2. CI runs type checks and targeted tests.
  3. CI builds the web for the staging Convex deployment and deploys it to the dedicated staging Vercel project. Build-time VITE_CONVEX_URL and VITE_AUTH_URL must be staging values.
  4. CI builds immutable agents and runner images tagged with commit SHA; it sets RIVET_RUNNER_VERSION from the release identity.
  5. CI runs Ansible convergence, updates the Compose release to those exact image digests, and executes docker compose up -d.
  6. 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.
  7. Rollback means redeploying the previous image digests and restoring the corresponding Vercel deployment—not rebuilding mutable latest images.

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:

  1. Create the separate staging Convex deployment and its deploy key; configure deployment-scoped secrets and SITE_URL.
  2. Correct the auth/webhook origin seams:
    • Add Vercel production rewrites for /api/auth/* to the staging Convex Site URL; Vites current proxy only applies to local development.
    • Make the Puter webhook target the staging Convex Site HTTP action directly, rather than the web origin.
  3. Add Vercel React Router support and a dedicated staging deployment configuration.
  4. 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.
  5. Add health routes/checks, resource limits, volume backup, image-digest deployments, and RIVET_RUNNER_VERSION.
  6. Add infra/pulumi and infra/ansible, then CI environments with protected staging secrets.
  7. 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 .env files 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