hecks

Clients share one database instance, each in its own database

Status: Accepted; phase 1 is built. Date: 2026-10-06. Builds on ADR 0090 and the AwsBox deploy kind.

Context

An AwsBox deploy generates two stacks: a box and a dedicated RDS instance (rds.yaml). A second client site would add a second instance, with its own backups, alarms, patching window and monthly cost, for a database that holds a few hundred megabytes. The user wants one RDS instance shared across clients, owned by the platform rather than by any one client.

Three facts about the code today decide the shape:

Decision

  1. One platform-owned instance. A new stack, generated like rds.yaml but not tied to a client, holds every client’s database. The platform repo owns its world file. Clients refer to it by stack name.
  2. A database and a login role per client. Each client gets a database and a role that owns only it. The role is not a superuser and has no rights on another client’s database. A client’s data therefore dumps, restores and hands over on its own, and one client’s bug cannot read another’s rows. The era write-fence, which a superuser skips, is in force for it.
  3. A client’s world says shared_database. deployed_to("AwsBox") gains shared_database "<stack>". With it the generator writes no rds.yaml; the Makefile and deploy-box.sh read the endpoint and security group from the shared stack; the sizing settings that belong to the instance (database_class, storage_gb, engine_version, backup_days) are refused. This is wiring, so it lives in the world (the hecksagon side); the domain’s bluebook is unchanged.
  4. The containers read the client’s own secret, never the master’s. The secret is named <stack>/database and holds {username, password, host, port, dbname}. The box’s instance role reads that one secret only. The master secret is read by the operator who provisions, from a bastion, never by a box.
  5. provision-database.sh is generated for the client. It creates the role and the database if they are absent, owned by that role, with a generated password of letters and digits only (the host does not percent-decode), and writes the client secret. Run again, it changes nothing; --rotate replaces the password. It runs from a machine that can reach the instance, the way restore-to-rds.sh does.
  6. The host and the CMS boot script read username from the secret, defaulting to postgres so a dedicated instance behaves as before.
  7. Moving a client onto the shared instance is a data copy, not a deploy. restore-to-rds.sh copies the client’s schemas into its new database as its own role; verify-copy.sh compares them; the cutover is a roll with the new DB_HOST and secret. Each step is run by a person, and the cutover is ask-first.

What a client’s world says

deployed_to("AwsBox") do
  region "us-east-1"
  containers [{ name: "web", port: 8080 }, { name: "domain", port: 8082 }]
  shared_database "hecks-platform-rds"   # the platform's instance, by stack name
  database_name "acme"                   # this client's database, and the role that owns it
end

The generator then writes provision-database.sh and a make provision BASTION=i-... [ROTATE=--rotate] target, and no rds.yaml. make stacks deploys only the box; deploy-box.sh finds the secret acme/database by name. database_class, storage_gb, engine_version and backup_days with shared_database are refused.

Phases

  1. Client side (this ADR’s build): shared_database, provision-database.sh, the host and boot-script username, specs and docs.
  2. The shared instance’s generator (built): deployed_to("AwsSharedDatabase") writes the instance’s rds.yaml, a Makefile and a README; the platform repo declares the world.
  3. Lifeadelics moves (done 2026-10-07): rehearsed twice, then cut over: snapshot, a pre-copy, a write freeze (about ten minutes of downtime in all), a final copy as the master, ownership handed to the client role, the box’s secret access pointed at the new secret, a roll, and the site’s full smoke (120 checks) and inbox canary on the real hostname.
  4. Emaho onboards on it instead of a dedicated instance.

Consequences

Rehearsal findings

A rehearsal copy of a real site’s data into its own database on the shared instance (2026-10-07) changed step 7:

restore-to-rds.sh takes the master secret as the target and a CLIENT_ROLE to hand ownership to; that change is in docs/plans/first-deploy-gaps.md (row 14).

The box rehearsal

A throwaway box (Rehearsal=true, no public address) rolled against the rehearsal database as the non-superuser client role, with images built from the current release:

The production move