feat(schema): M4.1 — tenant_registry schema + migrate binary #6
Reference in New Issue
Block a user
Delete Branch "feat/m4.1-schema"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What
M4.1's full deliverable:
PLATFORM_ARCHITECTURE.md §5cschema as a singlegolang-migratemigration, a standalonecmd/migratebinary, and testcontainers integration tests that exercise it against a real Postgres 16.tenants,tenant_projects,tenant_products,tenant_idp_config,api_keys,audit_log.tenant_status,tenant_kind,idp_kind,tenant_project_status.cmd/migrate: embedded SQL viamigrations/embed.go; subcommandsup/down/version/force. Ships as a self-contained Orca init container in prod.make migrate-up / down / down-all / version / create NAME=…,make test-shortfor environments without Docker.Why
M4.1 acceptance:
make migrate-upon a fresh Postgres produces the documented schema. M4.2 (full REST surface + pgx-backed store) builds on top of this without changing the schema.Linked milestone: M4.1
How
0001_init.up.sql) lands the entire spec instead of N small migrations. Reasoning: this is the initial schema, not an evolution — splitting it across 6 files makes the round-trip review harder and the migration table noisier. Future schema changes get their own numbered pairs.tenant.slugregex enforces^[a-z0-9][a-z0-9-]{1,38}[a-z0-9]$so the portal can trust slugs everywhere downstream.tenant_productsis keyed on(tenant_id, product)so a product entitlement is either on or off, not duplicated.audit_logusesON DELETE SET NULLfortenant_idso forensic history outlives a tenant delete.tenants_status_idx,audit_log(tenant_id, created_at DESC),audit_log(product, …),audit_log(actor_id, …), partial index on non-revokedapi_keys.updated_attriggers on the four mutable tables — no application-layer code path can forget to touch the column.migrations/embed.godeclarespackage migrations+//go:embed *.sqlso bothcmd/migrateand (future)cmd/servership the SQL as part of the binary. No file mounting at runtime.golang-migrateas a library, not the CLI — keeps the install surface small (nogo installstep in the Dockerfile), keeps the version pinned ingo.mod.Test plan
go test -race ./...— 6 unit tests + 3 testcontainers tests, all green (~25s total)make build→ static binary;make build-migrate→ static migratormake migrate-upagainst the dev Postgres works end-to-end (verified locally)DROPstatementsRisk
Blast radius: repo-local. No service consumes the schema yet — M4.2 is the first consumer. Stage/prod databases don't exist yet (M1.2 brings VMs; M3.1 brings Infisical for the DB URL).
What could break:
[A-Z_.]. If anyone wants to import existing customers with non-conforming slugs, they'll need to either rewrite or relax. Caught byTestSlugConstraint.audit_log SET NULLon tenant delete is a deliberate choice — alternative is CASCADE (delete audit too). Talked through in code comments. If we later need full erasure for GDPR, do it in a separate redaction migration.Rollback plan:
make migrate-down-allin dev. The down migration is symmetric and tested.Checklist
DATABASE_URLis read from env; Makefile defaults at the dev DSN, prod resolves via Infisicalmigrate upis a no-op when on-versiona27072b6d0to8a35942aea8a35942aeato2a05dfc166PLATFORM_ARCHITECTURE.md §5c schema, end-to-end: enums: tenant_status (demo/trial/active/frozen/archived), tenant_kind (customer/demo), idp_kind (oidc/saml), tenant_project_status (active/archived) tables: tenants id/slug/name/status/kind/plan/erp_id/ stripe_id/trial_ends_at/contract_dates/ sales_owner tenant_projects sub-tenancy (GCP-Project style); opt-in via product manifest.supports_projects=true tenant_products tenant ↔ product matrix + JSONB config tenant_idp_config enterprise SSO (OIDC/SAML metadata) api_keys argon2 hash + prefix + scopes + revoked_at audit_log Retraced-compatible; indexed for cross- product filtering per §8.4 triggers: updated_at auto-bump on every mutable table fks: ON DELETE CASCADE for owned rows; SET NULL for audit_log cmd/migrate (new binary): golang-migrate as a library with migrations embedded via migrations/embed.go; subcommands up/down/version/force. Ships as a self-contained Orca init container in prod. Tests (require Docker; gated by -short): TestMigrate_upDownRoundTrip schema → 6 tables + 4 enums; down→ empty; up-after-down clean TestSeed_canInsertAndQuery insert across every table; FK cascade; audit_log SET-NULL keeps the row TestSlugConstraint regex rejects too-short / leading dash / trailing dash / uppercase / underscore Makefile: migrate-up/down/down-all/version/create NAME=...; test-short to skip integration when Docker isn't around; build-migrate for just the migrator. CI: pin golangci-lint to v2.12.2 (Go 1.25-compatible) + bump golangci-lint-action to v7 (v6 rejects v2.x). The handler-layer in-memory store is unchanged; M4.2 swaps it for the pgx-backed implementation against this schema. Refs: M4.12a05dfc166tof9e9f0e21b