Infrastructure
Infrastructure as Code — infra/ (Pulumi, TypeScript)
Infrastructure is managed using Pulumi with TypeScript, spanning Google Cloud Platform (compute, registry, secrets), Neon (Postgres), and Cloudflare (DNS + CDN). State lives on Pulumi Cloud — no self-managed state bucket.
Every third-party in the stack now has a first-class Pulumi provider, so there are no "manual setup notes" resources. Neon is provisioned by the Neon Pulumi provider, GCP by the Google provider, and DNS by the Cloudflare provider.
Directory Structure
infra/
├── index.ts # Main Pulumi program — wires the modules together, stack exports
├── gcp.ts # Cloud Run (web-app/marketing/api/worker), Load Balancing + Cloud Armor, GCS, Artifact Registry, Secret Manager, WIF, BigQuery
├── restate.ts # Restate Cloud wiring — environment config + request-signing key (Secret Manager); the runtime itself is managed off-cluster
├── neon.ts # Neon project (Scale, hipaa:true), branches, roles
├── cloudflare.ts # DNS records, proxied CDN endpoints
├── tailscale.ts # Tailscale subnet router (GCE VM) + internal ALB — non-prod tailnet access
├── Pulumi.yaml # Project configuration
├── Pulumi.dev.yaml # Dev stack config (placeholder values)
└── package.json # Pulumi + provider dependencies
State Backend — Pulumi Cloud
Pulumi state and secrets encryption are hosted on Pulumi Cloud (app.pulumi.com). This gives the team drift detection, per-stack history, and a shared encryption key without operating a state bucket.
| Stack | Purpose |
|---|---|
prod | Production GCP + Neon + Cloudflare |
dev | Shared non-prod resources |
Per-PR preview resources (Cloud Run web-app-pr-<n> / api-pr-<n> and the Neon pr-<n> branch) are not managed by Pulumi — they are created and destroyed directly by GitHub Actions. See PR Environments.
GCP Resources — gcp.ts
Cloud Run Services
Four services, all containers from Artifact Registry:
| Service | Source | Notes |
|---|---|---|
web-app | ui/web-app (Vite + TanStack) | Applications UI; hosts Better Auth + Drizzle → Neon |
marketing | ui/marketing (Next.js 14) | Public marketing site; no auth, no PHI |
api | backend/api (FastAPI) | Python HTTP API → Neon |
worker | backend/worker (Restate handlers) | Durable-execution / async job handlers → Neon; invoked by Restate Cloud over signed HTTPS, suspend-to-zero between steps |
| Setting | Value | Rationale |
|---|---|---|
maxScale | 10 | Cost control; adjust based on load |
timeoutSeconds | 3600 | worker only — 60-minute ceiling for long WellSky sync steps. web-app / api / marketing keep normal request timeouts (default 300s). |
| Image source | Artifact Registry | Same-project, no cross-project IAM needed |
Cloud Run IAM
- Public invocation is allowed for the marketing site, the
/healthliveness probe, and theworker's Restate ingress path only. Theworkerendpoint is public at the Cloud Run IAM layer because Restate Cloud is an off-GCP caller — it is authenticated in the handler by verifying Restate Cloud's request signature, not by Cloud Run IAM.
Production: Lock down Cloud Run IAM with proper authentication (--no-allow-unauthenticated, access via IAM / IAP). The open /health endpoint is illustrative only.
Restate Cloud wiring — restate.ts
The Restate durable-execution runtime is Restate Cloud — a fully managed, serverless Restate environment (region us), not a self-hosted server. There is no GCE VM, no persistent disk, and no Rust node to operate: Restate Cloud holds the durable state and drives execution, invoking the worker's HTTP handlers and suspending them to zero between steps. restate.ts therefore provisions no compute — it only wires the connection: the Restate Cloud environment endpoint/region and the request-signing public key, materialized into Secret Manager for the worker.
- Runtime: managed by Restate Cloud off-GCP. Because it is serverless there is no always-on node in our VPC to size, patch, or pay for at idle.
- Invocation: Restate Cloud calls the
worker's public HTTPS endpoint and each request is signed with the environment key; theworkerverifies the signature in-handler. Handlers suspend between steps, soworkerstill scales to zero when idle. - Config: the
workerservice readsRESTATE_*(Restate Cloud environment endpoint + identity/signing keys) from Secret Manager. See Environment Variables and Backend.
No PHI is shared with Restate Cloud (initial posture). Any PHI that must be journaled is encrypted at the serde boundary inside the handler (GCP-KMS-backed encrypting Serde), so Restate Cloud's journal and virtual-object state hold ciphertext only, and the primary entity identifiers are opaque, short-lived IDs — so keys, trace IDs, and call structure stay PHI-free and non-correlatable. Restate Cloud is therefore treated as an encrypted conduit and needs no vendor BAA. See HIPAA Compliance and Backend.
Artifact Registry
- Docker image repository for the
web-app,marketing,api, andworkerservices. - Images are tagged with commit SHA for traceability.
Secret Manager
- Runtime store for application secrets, materialized from Pulumi ESC and mounted into Cloud Run at deploy time. See Secret Management.
Workload Identity Federation
- GitHub Actions authenticates to GCP via Workload Identity Federation (GitHub OIDC) — no long-lived service-account JSON keys are stored as secrets.
BigQuery Dataset
- Name:
raw_data - Location: US
- Landing zone for the incremental Neon → BigQuery extract-load (see Data Pipeline).
Object Storage — GCS (user uploads / documents)
User-uploaded files and generated documents live in Google Cloud Storage, inside the trust zone:
- CMEK — buckets are encrypted with customer-managed encryption keys (Cloud KMS), not just Google-default keys.
- Signed URLs — clients never get direct bucket ACLs; the API mints time-limited signed URLs for upload/download so access is scoped and auditable.
- No public buckets. Uniform bucket-level access; PHI-bearing objects stay private and are served only via signed URLs.
- PHI-bearing uploads live in GCS inside the trust zone; the full PHI-at-rest list (Neon · GCS · Vertex AI · WellSky · the governed M365 plane) is on the HIPAA Compliance page.
Neon — neon.ts
Neon is managed by Pulumi via the Neon Pulumi provider — the project, its HIPAA posture, branches, and roles are all declarative resources:
- Project on the Scale plan, region AWS
us-west-2(same metro as Cloud Runus-west1to minimize cross-cloud egress). hipaa: trueset on the project (irreversible; setsaudit_log_level=hipaa). The BAA is self-serve on Scale — no separate add-on.- Extensions:
pgvector(embeddings) andpg_session_jwt(Better Auth JWT → RLSauth.user_id()). - Roles & authorization: least-privilege application role(s). Neon RLS is the enforcement floor — the last-line database guard that holds even if app code is wrong — while Cerbos (a self-hosted, stateless PDP running in-boundary) is the app-level policy decision point for richer, contextual authorization. RLS schema + migrations live in the top-level
schema/dir (owned by Drizzle Kit); Cerbos policies live in the top-levelpolicies/dir. RLS is the floor, not the authorization source of truth. - Branches: the primary branch plus the copy-on-write branch-per-PR model — ephemeral
pr-<n>branches are created by CI, not Pulumi.
The Neon Data API (PostgREST) is not enabled for PHI paths — it sits outside the HIPAA boundary. Application and API traffic use direct Postgres connections. See HIPAA Compliance.
Edge & WAF — split by PHI exposure
The edge is deliberately split so that PHI never transits a vendor we don't need a BAA with:
| Surface | Fronted by | Why |
|---|---|---|
| Marketing site + DNS (no PHI) | Cloudflare (proxied CDN + DDoS + DNS) | Static, public, PHI-free — Cloudflare's CDN/WAF is a good fit and needs no BAA because no PHI flows through it. |
| Web app + API (carry PHI) | GCP Cloud Load Balancing + Cloud Armor (WAF) | Keeps the PHI path entirely inside the Google BAA boundary. PHI never transits Cloudflare, so no Cloudflare Enterprise / Cloudflare BAA is required. |
Do not proxy the PHI-carrying web-app/API through Cloudflare. Terminate and protect those origins with Cloud Load Balancing + Cloud Armor so all PHI stays within GCP. Cloudflare handles only the marketing surface and DNS.
Cloudflare — cloudflare.ts
DNS and CDN are managed with the Cloudflare Pulumi provider:
- DNS records for the apex + subdomains. Marketing points at its Cloud Run service via the Cloudflare proxy; the web-app / API hostnames are DNS-only (grey-cloud), resolving to the GCP load balancer so their traffic bypasses Cloudflare entirely.
- Proxied (orange-cloud) endpoints for CDN + DDoS protection on the PHI-free marketing surface only.
Tailnet Access — Tailscale (non-prod dev surfaces)
Every non-production surface — per-PR previews, the shared dev / staging env, and the Coder remote-dev workspaces — is reachable only from the company tailnet (Tailscale). Production is unaffected. One tailnet is the single "dev network," so there is no second access mechanism to operate.
Shape — one internal LB, wildcard DNS, tailnet-only:
- Preview / staging Cloud Run services deploy with
ingress = internal-and-cloud-load-balancing— no public*.run.appURL. They are reachable only through the internal load balancer. - One regional internal Application Load Balancer (a private VIP in the VPC) does host-based routing. A wildcard Cloud DNS private zone
*.pr.internalresolves to the LB VIP, so there is no per-PR DNS record — CI adds a serverless NEG + one URL-map host rule per PR (web-app-pr-<n>.pr.internal,api-pr-<n>.pr.internal) and removes them on teardown. One LB, N backends. - A Tailscale subnet router — one always-on node (small GCE VM;
infra/tailscale.ts) — advertises the route to the LB VIP into the tailnet and pushes split-DNS for*.pr.internal. A Tailscale ACL scopes reachability to the dev group. - Net effect: a dev on the tailnet opens
https://web-app-pr-42.pr.internal; it resolves to the internal LB, host-routes to the right service — and nothing off the tailnet can reach it (the LB has no public IP; the only ingress path is the subnet router). This is network-level isolation, stronger than a public endpoint gated by IAP.
The same tailnet reaches Coder. The Coder-in-private-subnet workspaces are reached over this same tailnet — one VPN for previews, staging, and remote dev.
Tailscale carries no PHI and needs no BAA. Previews / staging run on the non-PHI Staging Neon project, so no PHI ever transits the tailnet. The one path where PHI could ride the tunnel is Coder → Neon Production; Tailscale is WireGuard, end-to-end encrypted (its DERP relays pass ciphertext only and never see plaintext), so it is treated as a HIPAA conduit (like an ISP) — no Tailscale BAA required. Never terminate PHI plaintext on a Tailscale relay/coordination hop.
Production stays public. The prod app / API remain on GCP Cloud LB + Cloud Armor, and marketing on Cloudflare — only non-prod is tailnet-gated.
Caching — Redis deferred
There is no Redis in the initial stack. The two things a cache would carry are handled in-database for now:
- Better Auth rate-limit storage and light caching live in Neon (Postgres). Better Auth supports a database-backed rate limiter, which is sufficient at launch volumes.
- Add Memorystore (Redis) only when real pressure appears — hot-path read latency, rate-limit write contention, or session/cache volume that Postgres shouldn't absorb. It slots in as a managed GCP resource (in-boundary) when the need is demonstrated, not preemptively.
Deferring Redis keeps one fewer moving part in-boundary. Revisit when metrics (Neon CPU on rate-limit tables, p95 read latency) justify a dedicated cache.
Resource Organization
Following the code organization principle of one concern per file:
| File | Resources |
|---|---|
index.ts | Pulumi program entry point, module wiring, stack exports |
gcp.ts | All GCP resources (Cloud Run, Cloud Load Balancing + Cloud Armor, GCS, Artifact Registry, Secret Manager, WIF, BigQuery) |
restate.ts | Restate Cloud wiring — environment config + request-signing key (Secret Manager); the runtime is managed off-GCP |
neon.ts | Neon project (Scale, hipaa:true), branches, roles, extensions |
cloudflare.ts | DNS records and proxied CDN endpoints |
As the infrastructure grows, further split gcp.ts by service (e.g., cloudrun.ts, artifact-registry.ts, bigquery.ts, secrets.ts).
See also:
- Secret Management — Pulumi ESC + GCP Secret Manager
- PR Environments — ephemeral Neon branch + Cloud Run per PR
- HIPAA Compliance — Neon Scale + BAA,
hipaa:true