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.
This commit is contained in:
168
docs/DEPLOYMENT_PLAN.md
Normal file
168
docs/DEPLOYMENT_PLAN.md
Normal file
@@ -0,0 +1,168 @@
|
||||
# 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:
|
||||
|
||||
```text
|
||||
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](https://vercel.com/docs/frameworks/frontend/react-router)
|
||||
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](https://flueframework.com/docs/ecosystem/deploy/docker) · [Rivet runtime modes](https://rivet.dev/docs/general/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](https://developers.cloudflare.com/workers/framework-guides/web-apps/react-router/)
|
||||
|
||||
## 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](https://docs.convex.dev/production/multiple-deployments) · [Convex environment variables](https://docs.convex.dev/production/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_TOKEN` shared 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](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app)
|
||||
|
||||
## Staging topology
|
||||
|
||||
```mermaid
|
||||
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. |
|
||||
|
||||
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](https://rivet.dev/docs/self-hosting/docker-compose) · [Rivet production checklist](https://rivet.dev/docs/self-hosting/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](https://www.pulumi.com/docs/iac/concepts/state-and-backends/) · [Pulumi secrets](https://www.pulumi.com/docs/iac/concepts/secrets/)
|
||||
|
||||
Ansible is not redundant: it makes the Contabo host reproducible without pretending that SSH command resources are declarative infrastructure. [Ansible basic concepts](https://docs.ansible.com/projects/ansible/latest/getting_started/)
|
||||
|
||||
## Delivery workflow
|
||||
|
||||
### Local iteration
|
||||
|
||||
1. Keep using the current root `pnpm dev:tailscale` stack and personal Convex dev deployment.
|
||||
2. Use the Mac’s 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; 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.
|
||||
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](https://flueframework.com/docs/guide/database)
|
||||
|
||||
## 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](https://docs.convex.dev/production/multiple-deployments), [hosting on Vercel](https://docs.convex.dev/production/hosting/vercel), and [HTTP actions](https://docs.convex.dev/functions/http-actions).
|
||||
- [Vercel React Router](https://vercel.com/docs/frameworks/frontend/react-router) and [Vercel environments](https://vercel.com/docs/deployments/environments).
|
||||
- [Rivet self-hosted Docker Compose](https://rivet.dev/docs/self-hosting/docker-compose), [production checklist](https://rivet.dev/docs/self-hosting/production-checklist), and [version upgrades](https://rivet.dev/docs/actors/versions).
|
||||
- [Flue Node/Docker deployment](https://flueframework.com/docs/ecosystem/deploy/docker) and [durable database ownership](https://flueframework.com/docs/guide/database).
|
||||
- [Pulumi state](https://www.pulumi.com/docs/iac/concepts/state-and-backends/), [Pulumi secrets](https://www.pulumi.com/docs/iac/concepts/secrets/), and [Ansible](https://docs.ansible.com/projects/ansible/latest/getting_started/).
|
||||
Reference in New Issue
Block a user