Skip to content

Infrastructure & Deployment

Field Value
Status live (current-state; GCP production)
Owner OPS / DIR
Applies to How Partile runs and ships: local/dev topology, GCP production, Cloudflare edge, data-room publication, mobile release path, and superseded AWS staging evidence.
Last updated 2026-06-14
Evidence partile-ops: infra/envs/prod-gcp/README.md, generated-secrets.tf, locals.tf, main.tf, variables.tf, prod-gcp.auto.tfvars.example; partile-ops: infra/modules/gcp-{artifacts,network,data,security,secrets,runservice,ingress}/; selfheal PR-1 deployed remediation responder facts (infra/remediation/, atelier/selfheal/pr-1); partile-ops: infra/edge/cloudflare/README.md; partile-infra through PR-30 seed/purge tooling; partile-mobile through PR-26 plus physical iPhone auth/product QA; partile-cloudflare PR-5 data-room publication; ../control-register.md (SEC-05/08/09/16, PRIV-15, OPS-01/03/05/06, GOV-07).

Current State

Partile now has a GCP production environment in project partile-prod, region europe-west2, with public API hostname api.partile.app. The environment is a low-cost MVP production stack: Cloud Run for the API, Cloud Run Jobs for the worker tasks, Cloud Run for a private remediation responder in SHADOW mode, Cloud SQL PostgreSQL on private IP, Memorystore Redis on the private VPC, Secret Manager for runtime secrets, Cloud Monitoring/Pub/Sub for selected alert delivery, and Cloudflare in front of the Cloud Run origin.

There is still no real user data. Pre-launch testing may use explicitly tagged mock accounts (users.is_seed = true) and must purge them before GA. The old AWS ECS/Fargate staging path is preserved below as superseded evidence and a not-adopted alternative; it is no longer the current deployment path.

Local / dev topology

  • Docker Compose still runs api, worker, postgres, redis, and one-shot migrate for development. Health remains /health; readiness checks Postgres and Redis.
  • Config remains env-driven (APP_ENV, DATABASE_URL, REDIS_URL, RATE_LIMIT_*, RETENTION_*, MATCHING_*, MOBILE_AUTH_REDIRECT_URL, LinkedIn, and AI keys). Values are not committed.
  • LinkedIn OIDC issuer metadata defaults to https://www.linkedin.com/oauth, with LINKEDIN_ISSUER overridable for tests.

GCP production topology

flowchart TB
  internet[Internet clients] --> cf[Cloudflare DNS and Worker]
  cf -->|TLS to run.app origin| api[Cloud Run service api]

  subgraph gcp[GCP project partile-prod / europe-west2]
    api -->|private ranges via connector| sql[(Cloud SQL PostgreSQL 16 private IP)]
    api -->|private VPC| redis[(Memorystore Redis)]
    api -->|runtime secret refs| sm[Secret Manager]
    api -->|image pull| ar[Artifact Registry Docker repo]

    sched[Cloud Scheduler] -->|OAuth run| match[Cloud Run Job matching]
    sched -->|OAuth run| retention[Cloud Run Job retention]
    operator[Operator] -->|on demand| migrate[Cloud Run Job migrate]
    mon[Cloud Monitoring observe alert policies] --> notif[Notification channel 7003417658785730995]
    notif --> topic[Pub/Sub topic partile-prod-remediation-alerts]
    topic -->|push POST /pubsub| responder[Cloud Run service remediation-responder]
    responder -->|structured decision logs| logs[Cloud Logging]
    responder -. shadow audit only .-> api

    match --> sql
    match --> redis
    retention --> sql
    retention --> redis
    migrate --> sql

    build[Cloud Build] -->|push image| ar
    tf[Terraform] -->|remote state| gcs[(GCS bucket partile-prod-tfstate)]
    tf --> api
    tf --> sql
    tf --> redis
    tf --> sm
  end
Internet
  -> Cloudflare DNS / Worker route api.partile.app/*
  -> Worker rewrites origin host to the Cloud Run run.app host
  -> Cloud Run service: api (public ingress, managed TLS, min_instances=1)
       -> Serverless VPC Access connector
       -> Cloud SQL PostgreSQL 16 (private IP only, PSA, backups/PITR)
       -> Memorystore Redis (private VPC)

Cloud Scheduler
  -> Cloud Run Jobs: matching, retention
Manual/operator
  -> Cloud Run Job: migrate
Cloud Monitoring
  -> Notification channel projects/partile-prod/notificationChannels/7003417658785730995
  -> Pub/Sub topic partile-prod-remediation-alerts
  -> Push subscription POST /pubsub
  -> Cloud Run service: remediation-responder (private, ENFORCE=false)
Cloud Build
  -> Artifact Registry Docker image used by Cloud Run service/jobs

The architecture diagram is currently hand-authored from Terraform evidence in partile-ops: infra/envs/prod-gcp/. A future ops hardening pass could generate the diagram from Terraform outputs or state summaries so the picture cannot drift from infrastructure code.

Runtime services

  • API: Cloud Run v2 service api, public ingress, allUsers Run Invoker, managed TLS at the Cloud Run origin, Cloudflare TLS at the edge, one warm minimum instance, and bounded maximum instances. The service uses the VPC connector for private ranges and mounts the Cloud SQL Unix socket.
  • Worker model: the long-running local app.worker service is not deployed as production service. Production uses Cloud Run Jobs: matching every two minutes, retention every five minutes, and migrate on demand.
  • Remediation responder: Cloud Run service remediation-responder, private, URL https://remediation-responder-u5ylen7vtq-nw.a.run.app, environment ENFORCE=false. Cloud Monitoring sends selected observe alerts through notification channel projects/partile-prod/notificationChannels/7003417658785730995 to Pub/Sub topic partile-prod-remediation-alerts, then a push subscription calls POST /pubsub. In shadow mode the responder decides and audits but never mutates production.
  • Images: the backend image is built by Cloud Build and stored in Artifact Registry. The build path is scoped to Artifact Registry write, source/object read, and logging permissions per ops evidence; runtime identities do not need build permissions.

Remediation decision bounds

The responder is default-deny. Only two runbook paths are allowlisted: api_down may decide to restart the last-known-good API revision, and api_5xx_spike_post_deploy may decide to roll back to the previous revision. All unmatched alerts escalate. A circuit breaker caps potential action decisions at <=3 per hour per resource and guards against repeat-fingerprint thrash. Every decision is logged as structured JSON. Because ENFORCE=false, these are audit records and rollout evidence only; enforce mode is a future planned change tracked in ../registers/action-register.md.

Data and network

  • Cloud SQL: PostgreSQL 16, private IP only (ipv4_enabled=false), Private Service Access, private Google access, deletion protection, automated backups, point-in-time recovery, ENTERPRISE edition, db-custom-1-3840, and current low-cost ZONAL availability.
  • Redis: Memorystore Redis on the private VPC; no public endpoint. Current defaults are the low-cost MVP posture and are not an availability SLA.
  • Network: custom VPC/subnet, Private Service Access for managed services, and Serverless VPC Access for Cloud Run egress to private ranges.
  • Remote state: Terraform remote state is in private GCS bucket partile-prod-tfstate, prefix terraform/prod-gcp, with uniform bucket-level access and versioning. The state can contain generated secret material, so the bucket is Tier 2 operational evidence and must remain private.

Secrets and config

Terraform creates six Secret Manager secrets and injects them into Cloud Run:

  • ANTHROPIC_API_KEY
  • DATABASE_URL
  • LINKEDIN_CLIENT_ID
  • LINKEDIN_CLIENT_SECRET
  • REDIS_URL
  • SESSION_SIGNING_SECRET

The operator supplies only ANTHROPIC_API_KEY, LINKEDIN_CLIENT_ID, and LINKEDIN_CLIENT_SECRET. Terraform generates the database app-user password and session signing secret with random_password, composes the database and Redis URLs, and grants Secret Manager accessor only to the API and worker runtime service accounts. Plain non-secret config is injected as normal Cloud Run environment variables, including APP_ENV=production, LINKEDIN_REDIRECT_URI=https://api.partile.app/auth/linkedin/callback, retention and rate-limit knobs, and RATE_LIMIT_TRUST_PROXY_HEADERS=false.

IAM

  • Dedicated service accounts exist for the API, worker jobs, and Cloud Scheduler caller.
  • API and worker service accounts receive only the runtime project roles needed to read Artifact Registry images, connect to Cloud SQL, write logs, and publish metrics, plus per-secret Secret Manager accessor grants.
  • Scheduler invokes only the Cloud Run Job run endpoints via its dedicated OAuth identity.
  • Cloud Build is a build-time identity only; it does not run production traffic.
  • The remediation responder service account is recorded in source facts as …-remediation@partile-prod. It has roles/logging.viewer and roles/monitoring.viewer at project scope, plus roles/run.developer scoped only to the api Cloud Run service. It has no secret, IAM-admin, data-plane, or delete roles.
  • The Pub/Sub push invoker service account is recorded in source facts as …-remediate-push@partile-prod. It has roles/run.invoker on the remediation-responder service only.

Director production access path

The control-plane VM partile-control runs production GCP operations through the non-snap Cloud SDK at /opt/agent/otto/google-cloud-sdk, authenticated keylessly through the VM-attached director-agent@partile-prod service account. No GCP key files are required for this path. The known caveat is privilege separation: any process running as otto, including executors, can reach the attached service account via the metadata server. Per-agent identity or equivalent metadata-server isolation remains an open action before relying on stronger separation claims.

Cloudflare API edge

api.partile.app is fronted by a Cloudflare Worker tracked in partile-ops: infra/edge/cloudflare/. Cloudflare Free does not provide origin Host header override for the needed shape, so the Worker rewrites the request URL host to the Cloud Run run.app origin. Cloud Run still terminates managed TLS at origin; Cloudflare terminates the public edge connection. Cloudflare API tokens and account/zone identifiers are operator-supplied through shell/CI secret store only and are never committed.

Data-room publication path

The data-room intranet remains published at dataroom.partile.app using Cloudflare Pages + Access. The canonical source remains this Git-safe dataroom/ tree. The data-room publication path is separate from the production API edge.

Accepted Risks / Current Open Items

These are tracked in ../registers/action-register.md:

  • Domain Restricted Sharing exception: project-scoped iam.allowedPolicyMemberDomains was relaxed to allow allUsers Run Invoker on the public API. This is accepted for the consumer API because app access is protected by LinkedIn OIDC sessions, Cloud SQL is private-IP only, no admin plane exists, and the exception is scoped to the production project.
  • Pre-launch seed data in production: realistic mock travelers may be loaded for testing when tagged users.is_seed = true; they are not real PII and must be purged and verified before GA.
  • Cloudflare token rotation: an operator note says a Cloudflare API token appeared in chat history. Rotate it; do not record the token value here.
  • Staging before GA: production exists first; create a separate staging environment before GA.
  • HA before production SLA: current Cloud SQL is ZONAL and Redis is the low-cost posture. Upgrade to regional Cloud SQL / HA Redis before offering a production availability SLA.
  • Remediation shadow-to-enforce gate: the responder is deployed in shadow mode only. Turning on ENFORCE=true needs shadow-log review, circuit-breaker evidence, operator approval, and a rollback/change record.
  • Control-plane privilege separation: the director prod-access path is keyless, but VM-attached-service-account access is shared by convention among processes running as otto; per-agent identity remains open.
  • Restore drills / SLO / on-call / pentest: Cloud SQL backups and PITR are configured, but restore drills, on-call/SLOs, and third-party security testing remain open.

Superseded AWS Staging Path

The earlier AWS path is preserved because it explains previous design decisions and evidence. It is superseded / not adopted for the current production stack.

Intended topology was: ALB with ACM TLS, ECS/Fargate API and worker tasks, RDS PostgreSQL, ElastiCache Redis/Valkey, ECR images, SSM Parameter Store, and CloudWatch logs in eu-west-2. Ops PR-7 applied artifacts, including ECR, an ARM64 backend image, task definitions, SSM references, and ALB listeners. Ops PR-8 aligned the staging SSM rate-limit contract.

The first AWS live deploy remained blocked by an account-level ECS/Fargate RunTask hold. No services or migration ran there, and no real user data ever landed in that AWS stack. Future reviewers should treat it as historical evidence and a not-adopted alternative, not as the current runtime.

Mobile Release Context

The client remains React Native / Expo with EAS as the build/distribution path. The physical iPhone development build and real LinkedIn partile:// auth/deep-link smoke passed on 2026-06-08 after the LinkedIn issuer hotfix. Mobile PR-15 through PR-18 implement and physically verify the boarding-pass gate, camera scan, Photos/Files image import, sanitized confirmation, verified presence creation, and fail-closed negative paths. Mobile PR-19 through PR-22 add synthetic parser tests, stable non-sensitive selectors, hard-gated local/dev automation seams, and Maestro flow scaffolding. Mobile PR-23 through PR-26 add message composer, directory/stars, request-with-note handling with safety actions, and profile editing. Android device QA, mobile AI interview UI, and propose-to-meet remain future/operator-owned.

  • Controls: ../control-register.md (SEC-05/08/09/16, PRIV-15, OPS-01/02/03/04/05/06, GOV-07).
  • Open items / accepted risks: ../registers/action-register.md.
  • Policies: ../policies/backup-and-recovery-policy.md, ../policies/change-management-policy.md, ../policies/cryptography-and-secrets-policy.md.
  • Architecture map: architecture-overview.md. SDLC: sdlc-and-quality.md.