The v2.1.0 release-gate Phase-9 docker compose smoke run against a
fresh Postgres surfaced two real defects in the migration files that
testcontainers schema-per-test never exercised. Both reproduce by
running 'docker compose down -v && docker compose up --build'
against the current master tree.
Bug A — migration 000045_users_deactivated_at.up.sql is malformed.
The 000029 schema defines:
permissions (id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE,
namespace TEXT NOT NULL)
role_permissions (..., permission_id TEXT NOT NULL REFERENCES ..., ...)
But 000045 was written as:
INSERT INTO permissions (name) VALUES ... -- missing id + namespace
INSERT INTO role_permissions (role_id, permission, ...) VALUES ...
^^ wrong column name
On a cold-DB run this fails immediately with:
pq: null value in column "id" of relation "permissions"
violates not-null constraint
Fix: provide id + namespace columns, use permission_id (the actual
column name), ON CONFLICT (id) DO NOTHING. The new permission ids
follow the existing 'p-auth-*' prefix convention (p-auth-user-read +
p-auth-user-deactivate) used by 000029.
Bug B — migration 000029_rbac.up.sql is not idempotent post-000043.
000029 originally created actor_roles with:
UNIQUE (actor_id, actor_type, role_id, tenant_id)
Audit 2026-05-10 HIGH-10 closure / migration 000043 drops that
constraint and re-creates it WITH scope columns:
UNIQUE (actor_id, actor_type, role_id, scope_type, scope_id, tenant_id)
The migration runner (internal/repository/postgres/db.go::RunMigrations)
is naive — no tracker table — and re-runs every *.up.sql file on
every server boot. On the second-and-later boots, 000029's seed
INSERT for actor-demo-anon-admin still references the
pre-000043 constraint name in its ON CONFLICT clause:
ON CONFLICT (actor_id, actor_type, role_id, tenant_id) DO NOTHING
Postgres errors out with:
pq: there is no unique or exclusion constraint matching the
ON CONFLICT specification
Fix: pin the conflict target to the row's primary key 'id' column
(always present, never altered). The seed row's deterministic id
'ar-demo-anon-admin' makes ON CONFLICT (id) work under both pre-
and post-000043 schemas.
Why testcontainers schema-per-test missed these:
Each test in internal/repository/postgres/*_test.go spins up a
fresh schema and applies every .up.sql in order ONCE. The full
'000029 -> 000043 -> retry 000029' cascade never happens because
migrations don't re-run within a test. Phase-9 docker compose
smoke is the only test path that exercises the server-restart-
on-error retry, which is exactly the missing coverage.
Verify (sandbox): go test ./internal/repository/postgres/ PASS.
Workstation re-runs 'docker compose down -v && docker compose up'
to confirm both bugs are closed.
Audit 2026-05-10 MED-11 closure (foundation step).
WHAT.
Lays the schema + domain foundation for the MED-11 federated-user
admin surface:
1. Migration 000045 adds users.deactivated_at TIMESTAMPTZ (nullable;
non-NULL = deactivated). Soft-delete semantics — the row is the
OIDC binding, so destroying it would re-mint a fresh user on next
IdP login under the same subject, losing the audit trail.
2. Seeds 2 new catalogue permissions:
- auth.user.read (admin / operator / auditor)
- auth.user.deactivate (admin ONLY)
3. Extends User domain struct with DeactivatedAt *time.Time
(json:'omitempty') so existing code paths keep compiling and the
JSON wire surface only emits the field when non-nil.
WHY.
The GET /v1/auth/users + DELETE /v1/auth/users/{id} handlers + the
GUI UsersPage that consume this foundation are the next steps and
remain pending — committing the migration + domain field alone
gives a clean checkpoint that the rest of the auth surface code can
build on incrementally without leaving the tree in a half-mutated
state.
HOW.
migrations/000045_users_deactivated_at.up.sql:
- ALTER TABLE users ADD COLUMN IF NOT EXISTS deactivated_at TIMESTAMPTZ
- INSERT 2 permissions into permissions
- INSERT role_permissions rows (read in r-admin/operator/auditor;
deactivate in r-admin)
- Single BEGIN/COMMIT, idempotent (ON CONFLICT DO NOTHING)
migrations/000045_users_deactivated_at.down.sql:
- reverse-order DELETE + DROP COLUMN
internal/auth/user/domain/types.go:
- User.DeactivatedAt *time.Time, JSON tag omitempty.
VERIFY.
- go vet ./internal/auth/user/... ./internal/auth/oidc/...
./internal/repository/... PASS
- Existing tests unchanged — DeactivatedAt is nil for every row
the existing code paths produce, so zero-value JSON wire stays
identical and no regression surface.
Refs: cowork/auth-bundles-audit-2026-05-10.md MED-11
cowork/auth-bundles-fixes-2026-05-10/HANDOFF.md item 14