Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.cynsta.com/llms.txt

Use this file to discover all available pages before exploring further.

Spendra ships an on-prem package at infra/helm/spendra. The supported baseline is customer-managed Kubernetes with external Postgres, Supabase-compatible dashboard Auth, customer ingress/TLS, customer secret management, and customer observability/SIEM. Spendra does not bundle Postgres, an ingress controller, a secrets manager, a service mesh, Prometheus, or a SIEM. The chart deploys only the Spendra web, API/gateway, worker, and operational jobs.

Architecture

Deploy these components:
  • spendra-web: Next.js dashboard.
  • spendra-api: Fastify management API and OpenAI-compatible gateway.
  • spendra-worker: Graphile Worker background processor.
  • External Postgres 16, operated by the customer with HA, backups, PITR, and monitoring.
  • Supabase-compatible Auth/JWKS for dashboard sessions.
  • Customer ingress, TLS, optional mTLS/service mesh, secrets manager, log pipeline, and metrics scraper.
Recommended production starting point:
ComponentCPUMemoryAvailability
Web dashboard2 vCPU1 GBAt least two replicas behind ingress.
API/gateway2-4 vCPU each2 GB eachAt least two always-on replicas.
Worker2 vCPU1-2 GBStart with one replica; scale by queue depth and outbox lag.
Postgres4+ vCPU8+ GBCustomer-managed HA Postgres with PITR.

Required secret

Create one Kubernetes Secret and pass its name as secrets.existingSecret. Minimum keys:
kubectl create secret generic spendra-runtime \
  --from-literal=DATABASE_URL='postgres://...' \
  --from-literal=SPENDRA_ALLOWED_ORIGINS='https://spendra.example.com' \
  --from-literal=SPENDRA_METRICS_BEARER_TOKEN='...' \
  --from-literal=OPENAI_API_KEY='...' \
  --from-literal=SUPABASE_JWT_SECRET='...'
Common optional keys include SENTRY_DSN, OPENROUTER_API_KEY, GEMINI_API_KEY, ANTHROPIC_API_KEY, Azure OpenAI settings, Vertex AI settings, SMTP/notification provider secrets, and provider-account secret references. For production, manage this Secret through the customer secrets manager. External Secrets Operator, Vault Secrets Operator, AWS Secrets Manager, Azure Key Vault, GCP Secret Manager, and sealed-secret workflows are all acceptable as long as Kubernetes receives the expected environment variable names.

Build environment-specific images

Build the web image separately for each environment. The dashboard browser bundle reads NEXT_PUBLIC_* values at Next.js build time, so a web image built for integration must not be promoted unchanged into production. Use environment-specific tags such as 0.1.0-int and 0.1.0-prod:
docker build -f infra/web.Dockerfile -t registry.example.com/spendra/spendra-web:0.1.0-prod \
  --build-arg NEXT_PUBLIC_SUPABASE_URL=https://auth.example.com \
  --build-arg NEXT_PUBLIC_SUPABASE_ANON_KEY='<browser-anon-key>' \
  --build-arg NEXT_PUBLIC_API_BASE_URL=https://spendra.example.com \
  --build-arg NEXT_PUBLIC_SENTRY_DSN='<optional-browser-dsn>' \
  .

docker build -f infra/api.Dockerfile -t registry.example.com/spendra/spendra-api:0.1.0-prod .
docker build -f infra/worker.Dockerfile -t registry.example.com/spendra/spendra-worker:0.1.0-prod .
The API and worker image contents are environment-neutral, but tag all three images with the same release/environment tag because the chart uses one image.tag value. Runtime endpoints, database credentials, provider secrets, and operational tokens come from secrets.existingSecret.

Install

Render and inspect the manifests before installing:
pnpm onprem:render
pnpm onprem:chart:lint
Install:
helm upgrade --install spendra infra/helm/spendra \
  --namespace spendra \
  --create-namespace \
  --set global.imageRegistry=registry.example.com/spendra \
  --set image.tag=0.1.0-prod \
  --set secrets.existingSecret=spendra-runtime \
  --set auth.supabaseUrl=https://auth.example.com \
  --set api.publicApiBaseUrl=https://spendra.example.com
The chart fails rendering if secrets.existingSecret is not set. The Secret must include DATABASE_URL; production API startup rejects missing DATABASE_URL instead of falling back to an in-memory store. The chart runs database migrations, Graphile Worker migrations, and database smoke checks as Helm hooks by default. db:seed is disabled by default for on-prem. It inserts demo Acme data and a demo API key for local/demo environments, so it must not run in enterprise production installs. For an isolated demo environment only, explicitly set both jobs.seed.enabled=true and jobs.seed.allowDemoData=true; the seed hook is install-only and does not run on upgrades.

Ingress, TLS, and mTLS

Enable ingress with customer-owned hostnames and TLS secrets:
ingress:
  enabled: true
  className: nginx
  hosts:
    - host: spendra.example.com
      paths:
        - path: /
          pathType: Prefix
          service: web
        - path: /v1
          pathType: Prefix
          service: api
        - path: /management
          pathType: Prefix
          service: api
        - path: /health
          pathType: Exact
          service: api
  tls:
    - secretName: spendra-tls
      hosts:
        - spendra.example.com
Terminate public TLS at ingress or load balancer. If mTLS is required, enforce it in the customer ingress, API gateway, or service mesh. Spendra containers do not terminate mTLS directly. Do not expose /metrics publicly. Scrape it only from the private observability plane with SPENDRA_METRICS_BEARER_TOKEN.

Catalog updates and air-gapped mode

By default, the worker syncs model and tool catalogs from upstream sources on the configured schedule. Disable online sync in air-gapped deployments:
catalog:
  onlineSync:
    enabled: false
  offlineBundle:
    enabled: true
Create a bundle in a connected environment:
pnpm catalog:export --output spendra-catalog.json --database-url 'postgres://...'
Import it inside the air-gapped environment after migrations:
pnpm catalog:import --input spendra-catalog.json --database-url 'postgres://...'
Imported rows are upserted and catalog sync metadata is recorded with source offline_bundle.

Upgrade and rollback

Before every upgrade:
  1. Confirm Postgres backups and PITR are healthy.
  2. Capture a restore point or backup according to customer policy.
  3. Render the Helm diff or rendered manifests.
  4. Run the upgrade during a maintenance window if schema changes are destructive or large.
Upgrade:
helm upgrade spendra infra/helm/spendra \
  --namespace spendra \
  --reuse-values \
  --set image.tag=<new-version>
Application rollback uses Helm revision rollback:
helm rollback spendra <revision> --namespace spendra
Schema rollback is not automatic. Normal fixes use forward migrations. If a destructive migration must be undone, restore Postgres from PITR/snapshot to the pre-upgrade restore point and then redeploy the matching application revision.

Post-install acceptance

Verify:
  • API /health returns successfully.
  • API /metrics is reachable only from the private scraper with the metrics bearer token.
  • Dashboard login reaches onboarding or the active organization.
  • Worker jobs are present and processing.
  • A governed OpenAI-compatible request creates a settled ledger entry.
  • Audit records are written for key, policy, budget, role, and organization changes.
  • Catalog sync is either healthy online or imported from an offline bundle.