Status: Accepted; phase 1 is built. Date: 2026-10-06. Builds on ADR 0090 and the AwsBox deploy kind.
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:
DbSecurityGroupId and admits itself on 5432, and the containers get DB_HOST, DB_NAME and DB_SECRET_ARN. Everything downstream already goes through one secret and one host name.rust/host/src/main.rs) and the generated CMS boot script both log in as the literal user postgres. Sharing the instance with that credential would give every client’s containers the master password.restore-to-rds.sh, verify-copy.sh and the migration setting already copy and compare a set of schemas between two databases through a bastion, with a source and a destination named separately.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.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.<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.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.username from the secret, defaulting to postgres so a dedicated instance behaves as before.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.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.
shared_database, provision-database.sh, the host and boot-script username, specs and docs.deployed_to("AwsSharedDatabase") writes the instance’s rds.yaml, a Makefile and a README; the platform repo declares the world.pg_dump is its own database, which keeps the handoff promise (their data is theirs to take) a single command.A rehearsal copy of a real site’s data into its own database on the shared instance (2026-10-07) changed step 7:
pg_restore as the client role is refused (“query would be affected by row-level security policy”). The restore runs as the instance’s master, which bypasses the fence.BYPASSRLS.verify-copy.sh compared 243 tables with identical rows and structure.pg_hba error that looks unrelated; close them first.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).
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:
username and port from the secret. The images a site runs before the move log in as postgres, so they must be rebuilt first (a prerequisite of the move, as written above). The domain resolved its era as “use existing” (ten held eras, current era 10): the move mints no era.SITE_URL is the production hostname, so a preview iframe loaded from a loopback address is cross-origin. That is a limit of rehearsing without the real hostname.render-compose.sh replaces DB_HOST and DB_SECRET_ARN but not DB_NAME: the task definition still names the old database, so the rehearsal patched it by hand.DbSecretArn back to the old secret and roll the box with the old scripts and images.shared_database; the generated deploy scripts read the shared stack and the site’s secret. Until it did, a routine deploy would have pointed the site back at the old database.DbSecurityGroupId pointed at the shared group, then the temporary rules removed. A site’s box that is also the bastion must not be named as the shared stack’s bastion: its own ingress rule would duplicate it.umami, secret umami/database) on the shared instance; the copy needed pgcrypto created in public afterwards (the old database had it; pg_dump -n public does not carry an extension, and verify-copy.sh caught the 36 missing functions); the service reads its URL from one secret, so the switch was one secret and a recreate of its container (about 3.5 minutes of analytics downtime). After it, the box’s two hand-added rules to the old instance were removed, and the old instance’s group has no inbound rules. Rollback while the old instance stands: the old URL is kept as url_old in the same secret; re-add the two rules.