Skip to content

Architecture Overview

Field Value
Status live (current-state; reflects GCP production, infra PR-30 seed tooling, mobile PR-26, selfheal PR-1 remediation responder)
Owner INFRA / OPS / DIR
Applies to The Partile platform as a whole: backend, Cloud Run API/jobs/responder, datastores, mobile client, identity, observability, and the Cloudflare edges.
Last updated 2026-06-14
Evidence partile-infra backend through PR-30 plus smoke suite; partile-ops: infra/envs/prod-gcp/ and infra/modules/gcp-*; selfheal PR-1 deployed remediation responder facts (infra/remediation/, atelier/selfheal/pr-1); partile-ops: infra/edge/cloudflare/; partile-cloudflare PR-5; partile-mobile through PR-26 plus physical iPhone auth/product QA; ../control-register.md; ../registers/action-register.md; readiness/threat-model-and-auth-review.md; readiness/ml-event-taxonomy.md.

Current State

Partile is a professional-networking travel product: travelers with verified overlapping airport/time windows can discover each other, request to connect, and message after accept. The application is a single FastAPI backend plus scheduled background jobs over Postgres and Redis, with a React Native mobile client and LinkedIn as the identity provider.

The current production runtime is GCP in partile-prod / europe-west2. The public API hostname is api.partile.app, fronted by Cloudflare and backed by Cloud Run. Production also has a private Cloud Run remediation-responder in SHADOW mode (ENFORCE=false) that receives selected Cloud Monitoring alerts, decides from a runbook allowlist, and writes structured JSON audit logs without mutating production. There is still no real user data; pre-launch production-like tests may use tagged mock accounts that must be purged before GA.

Components

Component Tech Role State
API FastAPI / Python on Cloud Run HTTP API: auth, account deletion, consent settings, boarding-pass-gated presence, directory/ranking, connection requests, conversations/messages, profile/intent tags, key-gated AI rationale/interview, persistent connections, blocks, reports implemented; live on GCP Cloud Run with public ingress
Worker jobs Cloud Run Jobs Scheduled matching and retention jobs; on-demand migrate job implemented; scheduled via Cloud Scheduler for matching/retention
Remediation responder Cloud Run + Pub/Sub push Private remediation-responder service receives Cloud Monitoring alert notifications on POST /pubsub, applies default-deny allowlisted remediation decisions, circuit-breaker bounds, and structured JSON audit logging implemented in SHADOW mode (ENFORCE=false); decides and audits but never mutates
Database Cloud SQL PostgreSQL 16 System of record, consent records, app events, connection requests, profile/intent fields, cached rationales, anonymous ML snapshot tables, tagged seed-account flag live; private IP only; backups/PITR enabled
Cache / ephemera Memorystore Redis OAuth state, mobile handoff codes, rate-limit counters live on private VPC
Mobile app React Native / Expo Client; LinkedIn auth handoff; boarding-pass scan/import gate; messaging, directory, connect requests, profile editing, safety controls implemented through mobile PR-26; iPhone auth and boarding-pass product path physically verified; Android manual QA and mobile AI interview UI remain future
Identity LinkedIn OIDC Sign-in; source of sub, email, name, picture integrated; signed id_token verified via JWKS
API edge Cloudflare Worker + Cloud Run managed TLS Public api.partile.app route; Worker rewrites origin host to Cloud Run run.app host live; Cloudflare token rotation open item
Data-room edge Cloudflare Pages + Access Protected publication path for generated data-room intranet live and Access-gated
Local/dev Docker Compose + Cloudflare dev tunnel Local development and manual auth testing exists; not production
AWS staging path ECS/Fargate/RDS/ElastiCache/ALB/SSM Earlier staging target superseded / not adopted; deploy was blocked before any real data

Request flow (authenticated API call)

flowchart LR
  mobile[Mobile app] -->|HTTPS Bearer session| cf[Cloudflare Worker api.partile.app]
  cf -->|HTTPS origin host rewrite| api[Cloud Run API]
  api -->|hash lookup| redis[(Memorystore Redis)]
  api -->|SQL over private IP/socket| db[(Cloud SQL PostgreSQL)]
  api -->|HTTPS signed OIDC| linkedin[LinkedIn OIDC]
  api -->|HTTPS key-gated minimized prompt| anthropic[Anthropic Claude]
  scheduler[Cloud Scheduler] -->|OAuth run job| jobs[Cloud Run Jobs]
  jobs --> db
  jobs --> redis
  monitoring[Cloud Monitoring observe alerts] --> channel[Notification channel 7003417658785730995]
  channel --> topic[Pub/Sub partile-prod-remediation-alerts]
  topic -->|push POST /pubsub| responder[Cloud Run remediation-responder ENFORCE=false]
  responder --> audit[Structured JSON audit log]
Mobile client
   | HTTPS (Bearer opaque session token)
   v
Cloudflare Worker: api.partile.app
   | HTTPS, origin host rewritten to Cloud Run run.app host
   v
Cloud Run API
   |- rate-limit dependency (Redis fixed-window) ........ 429 + Retry-After
   |- require_current_user -> hashed session lookup
   |- object-level authorization enforced in SQL ........ non-visible -> 404
   |- block-separation predicate at read/derive paths ... blocked pair hidden
   v
Cloud SQL PostgreSQL (private IP/socket)      Memorystore Redis (private VPC)

Job flow

Cloud Scheduler
  -> matching Cloud Run Job  -> generate candidates / expire stale candidates
  -> retention Cloud Run Job -> hard-delete by retention class

Operator
  -> migrate Cloud Run Job   -> forward-only DB migrations

The production worker model is scheduled jobs, not a long-running Cloud Run service. The local/dev app.worker remains useful for Compose.

Remediation alert flow

flowchart LR
  uptime["Uptime check failing<br/>…/alertPolicies/4068533737113267562"] --> channel["Notification channel<br/>projects/partile-prod/notificationChannels/7003417658785730995"]
  fiveHundred["API elevated 5xx rate<br/>…/alertPolicies/4053361739307914661"] --> channel
  channel --> topic["Pub/Sub topic<br/>partile-prod-remediation-alerts"]
  topic --> push["Push subscription<br/>POST /pubsub"]
  push --> responder["Private Cloud Run<br/>remediation-responder<br/>ENFORCE=false"]
  responder --> audit["Structured JSON audit log<br/>every decision"]
  responder --> deny["Default-deny escalation<br/>for unmatched alerts"]
  responder --> shadow["Shadow allowlist decisions<br/>restart last-known-good or rollback previous revision"]

The two observe alert policies now also notify the remediation notification channel. The responder is intentionally shadow-only: it evaluates the api_down and api_5xx_spike_post_deploy runbook paths, applies a circuit breaker (<=3 actions per hour per resource plus repeat-fingerprint thrash guard), and audits the decision, but ENFORCE=false prevents production mutation.

Authentication topology

LinkedIn OIDC uses CSRF state in Redis with atomic GETDEL and a short TTL. The mobile app completes sign-in through a one-time handoff code exchanged for an opaque session token; no token is ever placed in a URL, and the redirect target is fixed. The LinkedIn id_token is verified locally against JWKS before the identity is trusted. OAuth access/ID/refresh tokens are never persisted. Existing provider subjects mapped to a deleted/suspended user are rejected before session issuance.

Trust Boundaries

Mobile --HTTPS--> Cloudflare Worker --HTTPS--> Cloud Run API
                                             | private VPC / Cloud SQL socket
                                             v
                                    Cloud SQL / Memorystore

Cloud Scheduler --OAuth--> Cloud Run Jobs --> private VPC stores
Cloud Monitoring --> Pub/Sub push --> remediation-responder (shadow) --> audit log
Cloud Run API/Jobs --HTTPS--> LinkedIn / Anthropic
Operator/deploy path --> GCP control plane / Secret Manager / GCS state
Control-plane VM --> GCP metadata server --> director-agent@partile-prod
# Boundary Current state Control / notes
1 Mobile -> API edge HTTPS to api.partile.app; Bearer opaque session token TLS, hashed sessions, 401 on bad/missing token, 429 preserved by mobile
2 Cloudflare -> Cloud Run HTTPS to Cloud Run managed-TLS origin; Worker rewrites origin host Cloudflare token rotation is an open action; no token value stored here
3 Cloud Run -> data stores Serverless VPC connector, Cloud SQL private IP/socket, Redis private VPC Data stores are not publicly reachable; Cloud SQL backups/PITR enabled
4 API/jobs -> LinkedIn / Anthropic HTTPS; LinkedIn signed-token verification; Anthropic key-gated/minimized Anthropic receives minimized rationale/interview prompts only when key set
5 Scheduler -> jobs OAuth token from dedicated Scheduler service account Scheduler invokes job run endpoints only
6 Operator/deploy path -> GCP Terraform, GCS remote state, Secret Manager, Artifact Registry, Cloud Build State and real secrets are Tier 2; labels only in Git
7 Monitoring -> remediation responder Cloud Monitoring notification channel to Pub/Sub push subscription to private Cloud Run remediation-responder SHADOW mode only; allowlist/default-deny/circuit breaker/audit log; enforce rollout tracked as an open action
8 Control-plane VM -> GCP /opt/agent/otto/google-cloud-sdk uses the VM-attached director-agent@partile-prod service account keylessly No key files; caveat: any process running as otto can reach the attached SA via metadata server until per-agent identity is introduced
9 Admin/moderation surface none exists yet must design authn/authz before any admin tool

Evidence

  • Backend service model, endpoints, scheduled job modules, ML snapshot boundary, boarding-pass-gated presence contract, message send, ranked directory, connection requests, profile/intent tags, key-gated AI rationale/interview, persistent connections, seed/purge tooling, and smoke suite: partile-infra through PR-30.
  • Production GCP topology, private data stores, Secret Manager, runtime service accounts, Cloud Run service/jobs, Cloud Scheduler, GCS remote state, and Cloudflare origin shape: partile-ops: infra/envs/prod-gcp/, infra/modules/gcp-*, and infra/edge/cloudflare/.
  • Self-healing operations topology: selfheal PR-1 deployed remediation-responder as a private Cloud Run service receiving partile-prod-remediation-alerts Pub/Sub push events from notification channel projects/partile-prod/notificationChannels/7003417658785730995.
  • Auth/session, account-erasure, and authz posture: readiness/threat-model-and-auth-review.md; controls SEC-01-10, SEC-12, SEC-15/16, PRIV-04/06/15, SAF-01/04, OPS-06 in ../control-register.md.

Known Gaps / Next Work

  • Client architecture direction — going native-first (Swift), decided 2026-06-17. The mobile client is being re-platformed from React Native/Expo to a pure Swift/SwiftUI iOS app (ios-native track, ios-app/PartileNative). Rationale: Partile's differentiating surfaces (boarding-pass Vision decode, Live Activities, share extension, geofencing, future UWB/App Clips, brand motion) are exactly where RN is weakest, and on-device testing surfaced RN-specific limits (share-extension memory wall). The RN app (partile-mobile) remains the running v1 until the native app reaches MVP parity, then cut over. Backend unchanged — the native client consumes the same api.partile.app. Phase-0 spike (PR-1) is built + CI-verified green (build + XCTest on a GitHub Actions macOS runner; the agentic native loop is proven). SDLC for native: build/test on GitHub Actions macOS runners (signed device builds + TestFlight via the vaulted App Store Connect API key). This is an in-progress migration, not yet the production client. Phase 1a (PR-2) — auth + session foundation — built + CI-verified green: a native API client for api.partile.app (bearer-token, same contract as the RN client), the LinkedIn web-auth handoff via ASWebAuthenticationSession → one-time-code exchange at POST /auth/mobile/exchangesession token in the iOS Keychain (kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, device-only) → Authorization: Bearer on calls, GET /auth/me bootstrap, 401-clear, logout. Same minimization boundary as the RN client: the token, the one-time code, and callback URLs are never logged/persisted outside the Keychain (director-audited: zero logging calls). No new retained data class; the native client is a new consumer of the existing auth contract. Phase 1b core loop (PR-3/PR-4) — CI-verified: native boarding-pass gate → on-device decode → POST /presence and a presence/home hero. The presence intake reuses the PD-012 contract (verification_source=boarding_pass_barcode, verification_level=document_verified, derived-only — unit-tested to exclude PII from the request) and the raw pass material stays transient; same minimization boundary as the RN client and the backend /presence contract is unchanged. No new retained data class. Native share extension + Live Activity (PR-5/PR-6) — CI-verified: a Swift Share Extension (own target + App Group group.io.partile.native) ingests a shared boarding pass and hands it — via the App Group shared container (not an unreadable sandbox URI) — to the same import→decode→presence flow; the shared file is transient and cleaned after import, derived-only, nothing logged. The Live Activity (ActivityKit widget) shows the user's own derived flight/presence on the lock screen / Dynamic Island with mock status (no flight API, no new data). Both extensions reuse the existing minimization boundary; no new retained data class. The native client now covers the core product loop end-to-end (auth → ingest [in-app + share] → presence → home → ambient presence).
  • No real users yet. GCP production exists, but only mock seed data may be used before GA and must be purged through the PR-30 seed lifecycle.
  • Staging environment still needed before GA. The current live environment is production; a separate staging environment is an open action.
  • Availability maturity is not yet an SLA. Cloud SQL backups/PITR are configured, but restore drills, SLO/on-call, regional Cloud SQL, HA Redis, and RTO/RPO proof remain open.
  • Remediation enforcement is not enabled. The responder is live in shadow mode only; ENFORCE=true requires a separate trust gate and change record.
  • Control-plane prod access is keyless but not per-agent isolated. The control-plane VM uses an attached service account with no key files, but any process running as otto can reach that identity via the metadata server.
  • Cloudflare token rotation is open because the operator recorded that a token appeared in chat history. The value is not stored in this data room.
  • LinkedIn PKCE/nonce not implemented - optional future auth hardening after signed id_token validation.
  • No admin/moderation plane - to be designed with proper authz before any moderation tooling.
  • AWS staging is superseded. Its previous ECS/Fargate RunTask blocker no longer blocks production, but the historical evidence remains in infrastructure-and-deployment.md.
  • Mobile device testing is partial. iPhone auth and boarding-pass paths are verified; Android install/manual QA, live Maestro execution, mobile AI interview UI, and propose-to-meet remain future/operator-owned.