Running your app against local twins
You have a twin running on localhost:4111. Now your application code needs to point at it in dev, while pointing at the real vendor in production. This is the substitution pattern — and it’s the core of integrating WonderTwin cleanly.
The principle
Section titled “The principle”For each external service your app talks to, the base URL is the variable. Everything else — API surface, SDK calls, auth shape — stays identical. The twin speaks the real vendor’s protocol.
So the wiring boils down to: read base URL from environment; default to the real vendor; override with the twin URL in dev/staging.
The pattern (TypeScript / Vite reference)
Section titled “The pattern (TypeScript / Vite reference)”The minimum viable shape:
import Stripe from 'stripe'
const baseURL = import.meta.env.VITE_STRIPE_BASE_URL || 'https://api.stripe.com'
export const stripe = new Stripe(import.meta.env.VITE_STRIPE_KEY, { host: baseURL,})Then in your dev environment:
# .env.local — checked into the repo as .env.local.example, never as .env.localVITE_STRIPE_BASE_URL=http://localhost:4111VITE_STRIPE_KEY=sk_test_anything # twins don't enforce real keysIn production, leave VITE_STRIPE_BASE_URL unset. The SDK falls back to the real Stripe API. Same code path, different runtime configuration.
Beyond one service: a registry
Section titled “Beyond one service: a registry”When your app talks to 3+ twinned services, the per-file import.meta.env.VITE_<SERVICE>_BASE_URL pattern starts to fragment. A central registry collapses it:
export const SERVICES = { stripe: { real: 'https://api.stripe.com', envBaseUrl: 'VITE_STRIPE_BASE_URL', envKey: 'VITE_STRIPE_KEY', }, posthog: { real: 'https://us.i.posthog.com', envBaseUrl: 'VITE_POSTHOG_HOST', envKey: 'VITE_POSTHOG_KEY', }, // …} as const
export type ServiceName = keyof typeof SERVICES
export function serviceUrl(name: ServiceName): string { const cfg = SERVICES[name] return import.meta.env[cfg.envBaseUrl] || cfg.real}
export function isTwinned(name: ServiceName): boolean { return Boolean(import.meta.env[SERVICES[name].envBaseUrl])}Each per-service wrapper then reads from the registry instead of hardcoding env names:
import { serviceUrl } from '../services/registry'import Stripe from 'stripe'
export const stripe = new Stripe(import.meta.env.VITE_STRIPE_KEY, { host: serviceUrl('stripe'),})Benefits the registry gives you:
- Single inventory — one place lists every external service your app touches and which are twinable.
- Type safety —
ServiceNamemakes invalid service names a compile error. - Build-time validation — a small script can assert every service in the registry has both env vars actually wired in your deploy pipeline (catches “added a wrapper, forgot the deploy step”).
- Debug visibility — render a
/admin/servicespage in dev/staging showing resolved URLs and twin-or-real status (see hosted-non-prod.md). - Production safety — the registry is the natural place to enforce “never resolves to a twin URL in a prod build” (see prod-no-twins.md).
Reference implementations
Section titled “Reference implementations”WonderTwin’s own apps dogfood this pattern. The current implementations are small and good starting points to copy:
wondertwin-web/apps/app/src/lib/posthog.ts— env-based PostHog with twin-posthog in staging, PostHog Cloud in prodwondertwin-web/apps/app/src/lib/logo-url.ts— same pattern for logo.dev- Service-registry package — TODO when the abstraction lands as
packages/services; this guide will link to it
Anti-patterns
Section titled “Anti-patterns”- Hardcoding twin URLs in committed code. The twin URL belongs in env config, never in source.
- No fallback default. If
VITE_STRIPE_BASE_URLis unset, your code should default to the real vendor — not crash. Production must work without any twin env vars present. - Stringly-typed service names. Without the registry’s type,
serviceUrl('stipe')(typo) silently returns the default for an unknown service. The registry pattern makes this a compile error. - Skipping the production safety check. Even with disciplined config, accidents happen. Add the explicit check from prod-no-twins.md.
TODO / coming later
Section titled “TODO / coming later”- Go reference implementation
- Python reference implementation
- Patterns for SDKs that don’t accept a base-URL override (HTTP proxying via
localhost) - Patterns for services with non-HTTP protocols (gRPC, WebSocket twins)
- Multi-tenant / per-request twin selection (advanced; uncommon)