6 · Add a database
Bind Postgres to ACME with a Kubernetes Secret — no DSN in your repo, no provider in your code.
A storefront needs to remember things. This chapter gives ACME a database without putting a password anywhere near your repository.
knext provisions nothing
Worth stating before the commands, because it explains the shape of everything below.
knext is engine-agnostic and creates no databases. You bring a Postgres — managed, serverless, or a CloudNativePG cluster in the same Kubernetes cluster — and knext binds its connection string into your pods.
That is a deliberate boundary, and it buys you something concrete: swapping providers later is a Secret change, with no change to ACME's code or config. Your app never learns which provider it is talking to.
Put the DSN in a Secret
A Kubernetes Secret is a named bag of values the cluster holds for you:
kubectl create secret generic acme-db \
--from-literal=DATABASE_URL='postgres://user:pass@host:5432/acme'The connection string now lives in the cluster. Not in your repo, not in your image, not in an environment file someone might commit.
Reference it by name
const config: KnativeNextConfig = {
name: 'acme',
// …registry, storage, scaling as before…
database: {
secretRef: { name: 'acme-db' },
},
};Redeploy:
npx kn-nextACME's pods now come up with DATABASE_URL in their environment.
Notice what you did not do: you never wrote the password in a file, and you never told the app which database provider it is using.
Reads and writes, separately
Add roSecretRef and knext binds a second connection string as DATABASE_URL_RO:
database: {
secretRef: { name: 'acme-db' }, // → DATABASE_URL (writer)
roSecretRef: { name: 'acme-db' }, // → DATABASE_URL_RO (reader)
},One Secret carrying both keys can serve both fields — the key names default to the variable names. This is how you send bounded-stale reads to a replica without your application code learning anything about topology.
spec.database owns DATABASE_URL and DATABASE_URL_RO. If you also try to set those names through
secrets.envMap, the apply is rejected rather than one silently overriding the other. A conflict
you can see beats a precedence rule you have to remember.
Querying it
@getknext/db is a typed client (Drizzle) that understands the writer/reader split — it routes
writes to DATABASE_URL and bounded-stale reads to DATABASE_URL_RO when present. See
Databases.
What is still wrong
ACME now sleeps. Its database does not — you are paying for an always-on Postgres behind an app that costs nothing while idle. That is the wrong shape, and it is the one most self-hosted setups are stuck with.
The next chapter fixes it.