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-infrathrough 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-*, andinfra/edge/cloudflare/. - Self-healing operations topology: selfheal PR-1 deployed
remediation-responderas a private Cloud Run service receivingpartile-prod-remediation-alertsPub/Sub push events from notification channelprojects/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-nativetrack,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 sameapi.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 forapi.partile.app(bearer-token, same contract as the RN client), the LinkedIn web-auth handoff viaASWebAuthenticationSession→ one-time-code exchange atPOST /auth/mobile/exchange→ session token in the iOS Keychain (kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, device-only) →Authorization: Beareron calls,GET /auth/mebootstrap,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 /presenceand 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/presencecontract 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 Groupgroup.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=truerequires 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
ottocan 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_tokenvalidation. - No admin/moderation plane - to be designed with proper authz before any moderation tooling.
- AWS staging is superseded. Its previous ECS/Fargate
RunTaskblocker no longer blocks production, but the historical evidence remains ininfrastructure-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.