Architecture
One Deployment, one Job, three external dependencies. That is the whole system.
Components
| Component | What it is |
|---|---|
| App Deployment | Stateless FastAPI workload serving UI and API on container port 8080 |
| Service | ClusterIP on port 80, targeting 8080 |
| Migration Job | alembic upgrade head, as a pre-install and pre-upgrade Helm hook |
| Backfill Job | Optional, one-off. Encrypts pre-existing plaintext rows |
The app is stateless, so it scales horizontally behind a single Service.
What sits outside the chart
PostgreSQL 14+. All durable state. Provisioned by you.
Object storage. Generated report artifacts: PDFs, radar images, exports. S3, GCS, or MinIO.
Identity. An OIDC issuer, an identity-aware proxy, or Google Sign-In.
The chart's postgresql.enabled, minio.enabled, and keycloak.enabled flags do not provision anything. They only select whether the app wires itself to an in-cluster service named <release>-<service> or to an external endpoint.
The analysis pipeline
Three stages, and only the third can leave your network.
- Scoring. Submitted text is scored against the lexicon and feature model baked into the image. Entirely local.
- Simulation. A seedable Monte Carlo run over latent state produces percentile vectors. Reproducible, and entirely local.
SIMULATION_SAMPLEScontrols the sample count. - Rendering. One LLM call converts the percentile vectors into Low / Medium / High trajectory text in the subject's voice.
Stage three is optional. Without OPENAI_API_KEY the app returns the same vectors in a templated form. Continuity across turns comes from persisted latent state fed back in, not from the model remembering anything, which is why the render call can be dropped without breaking the feature.
Naming
The chart names resources <release>-<chart>, which would render presponsieve-presponsieve. Set fullnameOverride: presponsieve and the Service is simply presponsieve. Every example and the Terraform module do this.
kubectl port-forward -n presponsieve svc/presponsieve 8080:80
Health
All three probes hit GET /healthz. Startup allows roughly 150 seconds, which covers CSV parse and database connect on a cold start.
The liveness probe is deliberately slack: 15 seconds by 6 failures. An analysis holds a worker thread for seconds at a time, and a busy pod should never be restarted mid-request.
Encryption
Sensitive columns are encrypted before they are written, using envelope encryption. KMS_BACKEND selects where the key-encryption key lives: k8s reads APP_KEK from the application secret, and aws, gcp, and vault delegate to an external KMS.
Searchable sensitive columns carry blind indexes, HMACed with INDEX_PEPPER, so equality lookups work without decrypting.
See Encryption.