Skip to content

Running twins in hosted non-prod environments

Local twins on a developer laptop are step one. The next move is hosted non-prod — a staging environment, a preview deploy, or a shared dev host — where twins are reachable by the staging copy of your application without each engineer running wt up locally.

A. In-process / per-job twins. Twins are spun up on-demand inside the same compute that runs the app (e.g. a docker-compose alongside your test runner, a sidecar in a preview deploy). Lifecycle is tied to the job/deploy.

  • Best for: short-lived preview deploys, integration test runs, ephemeral environments
  • Trade-off: every job pays the twin startup cost (~milliseconds, but it adds up); state isn’t shared between jobs

B. Long-running hosted twins. Twins run as persistent services on dedicated hosts, with stable URLs. Your staging app’s env vars point at those URLs (https://twins.staging.yourco.com/stripe, etc.).

  • Best for: team-shared staging, manual QA environments, demos
  • Trade-off: requires provisioning + reverse proxy + auth at the boundary; twins persist state across deploys

Most teams adopt B for shared staging and A for CI (see twins-in-ci.md).

The application code from step 2 doesn’t change. The deploy pipeline sets the env vars to the hosted twin URLs instead of localhost:

# Generic deploy step (CI tool agnostic)
- name: Build app for staging
env:
VITE_STRIPE_BASE_URL: https://twins.staging.yourco.com/stripe
VITE_POSTHOG_HOST: https://twins.staging.yourco.com/posthog
VITE_STRIPE_KEY: ${{ secrets.STAGING_STRIPE_KEY }}
run: pnpm build

Production deploys leave those _BASE_URL vars unset → SDK falls back to real vendors.

Reference architecture: WonderTwin’s own dogfood

Section titled “Reference architecture: WonderTwin’s own dogfood”

WonderTwin runs hosted twins behind a caddy reverse-proxy at api.wondertwin.dev/twins/<vendor> (e.g. api.wondertwin.dev/twins/posthog, api.wondertwin.dev/twins/logodev). The staging deploy of wondertwin-web sets:

VITE_WT_POSTHOG_HOST: https://api.wondertwin.dev/twins/posthog
VITE_WT_LOGODEV_BASE_URL: https://api.wondertwin.dev/twins/logodev

The production deploy of the same workflow leaves those unset, and the apps fall back to real us.i.posthog.com and img.logo.dev respectively. Single codebase, single deploy template, environment-determined behavior.

Add a /admin/services (or similarly-gated) page to your app that lists every service in your registry, the URL it resolved to, and twin-or-real status. Two reasons:

  1. In staging, you can verify the twin layer is actually wired before doing manual QA — no more “I thought I was hitting the twin but my staging API key got billed.”
  2. The page surfaces misconfigurations (missing env var, typo’d service name) that would otherwise be invisible.

Gate this behind admin auth or a build-time flag — it’s a development affordance, not a production surface.

  • Concrete provisioning patterns (Kubernetes Deployments, Cloud Run services, plain systemd, Fly machines)
  • Reverse-proxy / cert / auth patterns at the twin boundary
  • State persistence between deploys (when twins should be ephemeral vs. durable)
  • Resource sizing guidance
  • Multi-region staging considerations
  • Cost model — how much hosting twins typically costs at staging scale