Databases on Tawa

The Short Version

Declare databases in catalog-info.yaml. The builder provisions connection strings and injects them as environment variables. Your code reads from process.env. Identical across all environments.

spec:
  databases:
    - type: mongodb   # → MONGODB_URI
    - type: redis     # → REDIS_URL
    - type: neo4j     # → NEO4J_URI
    - type: postgres  # → DATABASE_URL (and POSTGRES_URI)

Koko first. Use koko for every read and write on a service database, in every environment. See Koko Data Access.

Supported Types

TypeEnv VariableNotes
mongodbMONGODB_URIRecommended for most services
redisREDIS_URLUse for caching and queues
neo4jNEO4J_URI (plus NEO4J_USERNAME and NEO4J_PASSWORD)Use for graph data
postgresDATABASE_URL and POSTGRES_URI (same value)For software that needs Postgres (e.g. SpiceDB). See PostgreSQL — needs a platform Postgres server first

Only these four types are provisioned. Any other type (such as mariadb, mysql, or sqlite) is logged as a warning and skipped — the build still completes successfully. That database simply isn't provisioned, so its env var is never injected.

Connecting

// MongoDB
import mongoose from 'mongoose'
const uri = process.env.MONGODB_URI
if (!uri) throw new Error('MONGODB_URI not configured')
await mongoose.connect(uri)

// Redis
import Redis from 'ioredis'
const url = process.env.REDIS_URL
if (!url) throw new Error('REDIS_URL not configured')
const redis = new Redis(url)

// Neo4j — NEO4J_URI, NEO4J_USERNAME, and NEO4J_PASSWORD are all injected
import neo4j from 'neo4j-driver'
const uri = process.env.NEO4J_URI
if (!uri) throw new Error('NEO4J_URI not configured')
const driver = neo4j.driver(
  uri,
  neo4j.auth.basic(process.env.NEO4J_USERNAME!, process.env.NEO4J_PASSWORD!),
)

// Postgres — DATABASE_URL carries the credentials
import pg from 'pg'
const url = process.env.DATABASE_URL
if (!url) throw new Error('DATABASE_URL not configured')
const pool = new pg.Pool({ connectionString: url })

PostgreSQL

Added in iec-builder#231. First consumer: SpiceDB (--datastore-engine=postgres --datastore-conn-uri=$DATABASE_URL).

spec:
  databases:
    - type: postgres
      name: authz        # optional — default is {service}-{env}

On every deploy the builder, using its admin connection:

  1. Creates (or, on redeploy, re-applies the stored password to) a login role svc_{service}_{env} — lowercased, - becomes _, NOSUPERUSER NOCREATEDB NOCREATEROLE.
  2. Creates the database {service}-{env} (or name:) owned by that role, and revokes CONNECT from PUBLIC so no other service's role can open it. An existing database is never recreated.
  3. Stores the password encrypted on the service record (databaseCredentials, type postgres) and reuses it on every later deploy.
  4. Writes the K8s secret {service}-db-postgres and injects DATABASE_URL and POSTGRES_URI, both postgresql://svc_…:…@{host}:{port}/{db}. They carry a password, so they reach the pod through the managed secret, never the Helm command line.
DetailValue
Database name ruleletters, digits, _, -; at most 63 bytes — anything else fails the deploy with a message
Credentialsstatic (Phase 1). Vault dynamic credentials are a follow-up
spec.egress.denyPrivatethe Postgres host:port is allowed when postgres is declared
tawa config set DATABASE_URL=…not reserved; if you set one and declare postgres, the provisioned value wins and the build log warns

Requires a platform Postgres server. The platform server is PostgreSQL 16 on the tawa builder host, reachable from pods at 10.124.0.3:5432 (VPC only — it listens on localhost and the VPC address, and 5432 is firewalled to 10.0.0.0/8; it is not reachable from the internet). Data lives on the attached volume (/mnt/volume_sfo3_01/postgresql/16/main), not the root disk. Connections are plaintext inside the VPC, like MongoDB and Redis. The builder provisions as the non-superuser admin role tawa_admin (vault alias tawa-postgres-admin). The builder needs DB_POSTGRES_ADMIN_URI (plus DB_POSTGRES_HOST, DB_POSTGRES_PORT, optional DB_POSTGRES_SSLMODE). When DB_POSTGRES_ADMIN_URI is unset the deploy still succeeds, nothing is injected, and the build log says:

[warn] spec.databases declares postgres, but DB_POSTGRES_ADMIN_URI is not configured on the builder — no Postgres database was provisioned and DATABASE_URL was not injected. See https://tawa.insureco.io/reference/databases

Backups: nightly and on-demand pg_dump -Fc backups are supported (iec-builder#233) — see database-backups.

Not yet supported for postgres (each tracked as a follow-up): koko backup / koko backups CLI support, Koko registration and koko query (Koko does not accept the type yet), consumesDatabase (refused with an error), credential rotation via /databases/:type/rotate, and tawa preflight (its schema still lists three types — it fails preflight, not the deploy).

Database Naming

MongoDB and Postgres databases are named {service}-{environment} by default:

ServiceEnvDatabase
my-apisandboxmy-api-sandbox
my-apiprodmy-api-prod

Override with a custom name:

spec:
  databases:
    - type: mongodb
      name: shared-data   # uses "shared-data" instead of "{service}-{env}"

Local Development

# .env.local
MONGODB_URI=mongodb://localhost:27017/my-api-dev
REDIS_URL=redis://localhost:6379/0
NEO4J_URI=bolt://localhost:7687

Or use tawa config pull to get the sandbox connection strings:

tawa config pull  # writes to .env.local

Vault (Dynamic Credentials)

MongoDB credentials are rotated automatically via HashiCorp Vault. The Vault Agent sidecar handles credential injection — your code reads MONGODB_URI as always, but the underlying credentials rotate without a redeploy.

Redis and Neo4j Vault rotation is pending (Phase 2c/2d).

Shared Databases (catalog 0.5.0+)

Services can share databases. The owning service declares sharedWith, the consumer declares consumesDatabase:

# Owning service (analytics-api):
spec:
  databases:
    - type: mongodb
      sharedWith: [reporting-service]

# Consuming service (reporting-service):
spec:
  consumesDatabase:
    - from: analytics-api
      type: mongodb   # → MONGODB_URI_ANALYTICS_API env var

Accessing Data in Running Services

Platform databases are not reachable from localhost. Use the koko CLI — it proxies all operations through the Koko API with authentication and safety controls.

# Install once
npm install -g koko-db

# Authenticate (shares tawa login session)
tawa login

Read Data

koko query products -s my-api -e prod --filter '{"active":true}' --limit 20
koko sample users   -s my-api -e prod --limit 5
koko stats          -s my-api -e prod
koko collections    -s my-api -e prod

Export / Import (EJSON — type-safe)

koko export outputs EJSON format: ObjectIds are preserved as {"$oid":"..."} and Dates as {"$date":"..."}. This means exports can be re-imported without ObjectId type corruption.

# Export prod → local file
koko export products -s my-api -e prod -o /tmp/products.ndjson

# Import into sandbox (koko import handles EJSON natively)
koko import products -s my-api -e sandbox -i /tmp/products.ndjson --upsert

# Import into local MongoDB (requires --legacy flag for $oid markers)
mongoimport --db my-api-dev --collection products --file /tmp/products.ndjson --legacy

Always use koko import over raw mongoimport — no flags to remember, types are handled automatically.

Copy Between Environments

# Direct server-side copy — fastest, no download
koko copy products --service my-api --from prod --to sandbox

See tawa-docs/conventions/koko-data-access.md for the full quickstart.

What NOT to Do

# ❌ WRONG: unsupported types — silently skipped (warning, non-fatal)
spec:
  databases:
    - type: mariadb    # not provisioned — logged as a warning and skipped
    - type: mysql      # not provisioned — logged as a warning and skipped

# ❌ WRONG: hardcoding connection strings
MONGODB_URI=mongodb://prod-host:27017/mydb  # in tawa config — let builder provision it

# ✅ CORRECT: declare in catalog, let builder provision
spec:
  databases:
    - type: mongodb

Last updated: October 6, 2026