Skip to main content

Architecture

One Deployment, one Job, three external dependencies. That is the whole system.

Components

ComponentWhat it is
App DeploymentStateless FastAPI workload serving UI and API on container port 8080
ServiceClusterIP on port 80, targeting 8080
Migration Jobalembic upgrade head, as a pre-install and pre-upgrade Helm hook
Backfill JobOptional, 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.

  1. Scoring. Submitted text is scored against the lexicon and feature model baked into the image. Entirely local.
  2. Simulation. A seedable Monte Carlo run over latent state produces percentile vectors. Reproducible, and entirely local. SIMULATION_SAMPLES controls the sample count.
  3. 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.