ADRs
ADR 0022 — Environments + branching model (trunk-based, ephemeral previews)
  • Date: 2026-05-25 (v1.0) · 2026-05-29 (v2.0 — supersedes v1.0)
  • Status: Accepted (v2.0). Supersedes the v1.0 env-branch model below.
  • Affects:
    • Branching workflow + worktree convention (Arno/Rules.md branch-strategy section)
    • CF Pages project arno (per-branch builds) + preview-proxy Worker
    • Workers: arno-prod (prod), arno-dev (dev), arno-test-<topic> (ephemeral per session)
    • DNS in zone arnomake.com: test-*.arnomake.com, dev.arnomake.com
    • Neon branches: production, dev, per-session preview branches
    • apps/api/wrangler*.toml, apps/api/src/index.ts (CORS), .github/workflows/*
    • GitHub branch protection on main
  • References: ADR 0021 (30K MAU ceiling), ADR 0029 (auto-migrate solo)

Changelog

  • 2026-06-02 v2.1: Phase C shipped — dev env pair landed (dev.arnomake.comarno-api-dev, Neon branch dev, Liveblocks project arno-dev). Naming reconciled with reality (arno-api-dev, parallel to existing arno-api / arno-api-test; v2.0 said arno-dev but prod was never renamed to arno-prod — Phase D never shipped). Introduced pair-atomic deploy invariant: every workflow (ci.yml, release.yml, deploy-test.yml) deploys backend → healthcheck → web in that order, so a failed backend never leaves a new web pointing at an old backend. Single web artefact serves all envs (host-based API switch in apps/web/src/lib/api-client.ts:HOST_API + packages/url-import-extension/arno-bridge.js).
  • 2026-05-29 v2.0: Pivot from environment-branching (v1.0) to trunk-based development + ephemeral preview environments. Drivers: branch drift + migration collisions observed under v1.0; opaque *.pages.dev preview URLs; requirement for multiple parallel sessions each with an isolated, on-brand, speaking preview URL. Environments become deploy slots of main's artifacts, not long-lived branches.
  • 2026-05-25 v1.0: Initial 2-env (prod + test-on-feat/**). Superseded.

Context

v1.0 mapped environments to long-lived branches (test = CF Pages preview of feat/**). In practice this produced exactly the failure modes trunk-based development is designed to avoid:

  1. Drift. feat/layout-grid ran 100+ commits behind main; deploy-test.yml needed a "warn if stale" step as a band-aid. Long-lived branches diverge.
  2. Migration collisions. Parallel branches both claimed the next sequential migration number → journal divergence (HANDOFF: "миграции применяются вручную т.к. журнал divergent").
  3. Rebuilt, not promoted. Prod was a different build than what was validated → "passed on test, broke on prod" risk.
  4. Opaque preview URLs. CF appended hash suffixes (feat-layout-grid-sl4j.arno-1hu.pages.dev); the bare alias served stale builds.

New requirement: the owner runs multiple parallel sessions (each effectively a concurrent contributor). Each needs its own isolated preview, simultaneously, on *.arnomake.com, with a speaking name.

This is the canonical big-org pattern: Trunk-Based Development + Continuous Delivery ("build once, promote the artifact") + ephemeral preview/review environments (cf. Google/Meta monorepo trunk, Vercel/Netlify/CF preview deployments, Netflix Spinnaker artifact promotion).


Decision

1. Branching — trunk-based

  • main = the only long-lived branch and source of truth. Protected.
  • feat/<topic> — short-lived, one per session/feature. <topic> is a short kebab slug; it also names the preview URL (test-<topic>.arnomake.com).
  • No long-lived dev/test/release branches. Environments are deploy slots (below), not branches.
  • Squash-merge PRs into main → linear history, one commit per feature (clean with many small session commits, trivial revert).

2. Session → branch lifecycle (Rule)

On a new session that recognizes "Arno project" and begins real work:

  • Create feat/<topic> from fresh origin/main + worktree Arno-<topic>/; push spins up the ephemeral test env.

Teardown triggers (reliable signals — "chat closed" is not observable):

  • Branch merged to main → auto-delete branch + worktree + ephemeral env (worker, Neon branch, DNS) via GH Actions on branch delete.
  • Session ended with zero commits → delete (no waste from pure Q&A/design sessions).
  • Stale (no activity N days) → periodic cleanup cron.

Pure discussion sessions create nothing (or auto-clean if empty).

3. Environments (deploy slots, not branches)

tierfrontend (user-facing)backend workerbackend URLNeonLiveblocks
test (per session)test-<topic>.arnomake.comarno-test-<topic> (ephemeral)arno-test-<topic>.vadimpianof.workers.devper-session branch (from production)arno-test
dev (main RC)dev.arnomake.comarno-devarno-dev.vadimpianof.workers.devdev (from main)arno-test
prodarnomake.comarno-prodapi.arnomake.comproductionarno

Naming note (intentional, documented to avoid confusion vs the common dev<staging<prod convention): here dev = the integrated main / release-candidate (more stable) and test-* = active WIP per session (churny). Backend workers follow arno-<tier>[-<topic>].

4. Frontend previews — speaking, parallel, on-brand, free SSL

  • Pattern test-<topic>.arnomake.com is a single-level subdomain → covered by free Universal SSL (*.arnomake.com). The test- prefix (not a <topic>.test. nesting) is deliberate: it avoids a 2-level wildcard that would require Advanced Certificate Manager ($10/mo).
  • Routing: a preview-proxy Worker on test-*.arnomake.com resolves the branch's latest CF Pages deployment (via CF API, cached) and proxies it — which also fixes the stale-alias problem. DNS for test-* managed per-branch by CI (or a scoped wildcard), without shadowing api/dev/apex (they have explicit records).
  • CF Pages auto-builds every branch, so frontend previews cost ~0 config; only the proxy Worker is added (free on existing Workers Paid).

5. Backend symmetry — ephemeral env per session

Backend mirrors the 3 tiers. The asymmetry vs frontend: CF Pages builds branches automatically, but each backend env must be deployed + secret-injected + given a Neon branch + torn down by CI. Accepted as the cost of true per-session isolation (eliminates shared-DB coupling between parallel sessions). Backend URLs are internal (workers.dev) — only frontends need .arnomake.com.

6. Promotion (gated, build-once)

  • Merge to main → auto-deploy to dev (dev.arnomake.com, RC).
  • Prod = manual workflow_dispatch "release" promoting the same artifact/SHA validated on dev (CF Pages promote + wrangler versions deploy), behind an environment-approval gate. Never auto on merge. Aligns with existing rules "backend deploy only from main" + "push to main only on explicit directive".

7. Migrations

  • Per-session Neon branch removes runtime collisions (each branch migrates its own DB).
  • Rebase-before-merge (branch-up-to-date rule) linearizes migration order at the single integration point (main).
  • Optional extra: timestamp/ULID migration IDs to minimize journal merge-conflicts — pending verification that Drizzle's sequential journal supports it; adopt only if it doesn't fight the tooling.

8. Guardrails — branch protection on main

PR required (no direct push) · status checks green (typecheck/test/e2e) · branch up-to-date with main required (kills drift) · squash/linear history · no force-push, no deletion · prod deploy only via the gated release workflow.


Rejected alternatives

AlternativeWhy rejected
Environment branches (GitFlow / v1.0)Drift + migration collisions (observed); rebuilt-not-promoted; scales badly with parallel contributors
<topic>.test.arnomake.com (nested)2-level wildcard not covered by Universal SSL → needs ACM $10/mo. Flattened to test-<topic>. prefix → free
Single preview slot (preview.arnomake.com, overwritten)Breaks the core requirement: multiple parallel sessions need simultaneous previews
Shared non-prod backend + DB for all previewsCouples parallel sessions on schema/data; chose per-session isolation instead
Keep prod worker arno-apiOwner chose full naming symmetry → arno-prod (one-time careful migration, §Implementation)
Per-PR ephemeral Neon + per-branch backend custom domainsBackend is internal; workers.dev URLs suffice — avoids per-branch DNS/route churn

Consequences

Positive: no long-lived-branch drift; clean parallel on-brand speaking previews at $0 extra SSL; true per-session isolation; artifact promotion (no prod surprises); scales to more sessions / a team; gated prod.

Negative / cost: the largest work item is CI orchestration of ephemeral environments (create: deploy worker + inject secrets + Neon branch + DNS; teardown on branch delete). Per-env secret injection. Preview-proxy Worker is a dependency in the preview path. Prod worker rename is a one-time prod-touching migration.

Mitigation: ephemeral orchestration written once, then automatic; if it proves too heavy, fall back to a shared non-prod backend for FE-only sessions (re-eval trigger).


Implementation (phased)

Phase A — config + CI (no prod impact)

  • apps/api/src/index.ts CORS allowlist += https://test-*.arnomake.com (regex), https://dev.arnomake.com
  • Rework deploy workflows: push feat/* → build + deploy arno-test-<topic> + frontend branch build + Neon branch; merge main → deploy arno-dev + dev.arnomake.com
  • GH Actions on branch delete → teardown ephemeral env
  • Branch protection on main

Phase B — preview routing

  • preview-proxy Worker on test-*.arnomake.com (resolve latest deployment per branch)
  • DNS strategy for test-* (per-branch records via CI, or scoped wildcard) — verify no shadowing of api/dev/apex

Phase C — dev env (✅ shipped 2026-06-02 v2.1)

  • arno-api-dev worker (apps/api/wrangler.dev.toml) — name aligned with arno-api / arno-api-test, not v2.0's arno-dev
  • ci.yml dev pair deploy block (migrate dev-Neon → backend → healthcheck → web)
  • release.yml reordered for pair-atomicity (backend → healthcheck → web)
  • Host-based API switch in api-client.ts + arno-bridge.js (one web artefact, three pairs)
  • Owner UI: Neon branch dev + Liveblocks project arno-dev + OAuth App Arno-dev + KV namespaces AUTH-DEV/SYNC-DEV + GH secrets DEV_DATABASE_URL / CF_WORKERS_SUBDOMAIN → wrangler secrets (see HANDOFF "Dev env one-time checklist")

Phase D — prod rename (prod-touching, separate careful step)

  • Create arno-prod (copy code + secrets + KV bindings) → verify on arno-prod.workers.dev
  • Move custom domain api.arnomake.com arno-apiarno-prod (brief cutover, low traffic)
  • Verify api.arnomake.com/health → delete old arno-api. Reversible (re-attach domain / redeploy).

Phase E — promotion + gate

  • release workflow (workflow_dispatch, environment approval) promoting validated main artifact to prod

Phase F — migrations

  • Verify Drizzle timestamp/ULID ordering; adopt if clean. Otherwise rely on per-branch DB + rebase.

Re-evaluation triggers

  • Parallel sessions / preview volume high → review CF + Neon free-tier usage
  • Ephemeral orchestration too heavy to maintain → shared non-prod backend for FE-only sessions
  • Team > 1 → formalize onboarding + CODEOWNERS
  • Secret rotation > monthly OR team > 2 → adopt a secrets manager (Doppler)

v1.0 (superseded — historical)

The original 2-env design (prod + test-on-feat/** preview, shared arno-api-test, Neon feat-layout-grid branch) is preserved in git history at the pre-v2.0 revision of this file. Key still-valid rationale carried forward: Neon branches (not separate projects) for cheap isolated DBs; secrets via wrangler secret put + GHA secrets (no Doppler at this scale); same Sentry project with env tag. Operational detail lives in docs/runbooks/environments.md.