- Second, deliberately separate migration (pure hand-SQL, no Prisma-
generated DDL): ENABLE/FORCE ROW LEVEL SECURITY plus a
tenant_isolation_policy for each of the three new tables, following
the pattern of 20260618112133_rls_policies (Auth-Kerntabellen)
rather than the RLS-exempt Tender* app-layer tables
- Group/ModuleGrant compare tenantId directly against
current_tenant_id(); GroupMembership has no own tenantId and follows
the PasswordResetToken join pattern (groupId IN (SELECT id FROM
Group WHERE tenantId = ...))
- migration-sql.spec.ts extended with a second describe block covering
both migration files (6x ROW LEVEL SECURITY, 3x CREATE POLICY, the
join vs. direct-comparison shape)
- Re-ran the Task-2 end-to-end proof after applying this migration:
identical result (USER without grant 403 + empty list, USER with
direct grant 200 + slug present, ADMIN 200) — the app's DB role
(tessera) is a Postgres superuser with rolbypassrls=true, so it
bypasses RLS as documented as an acceptable outcome by the plan;
RLS remains the defense-in-depth net for any future non-superuser
connection
- Group/GroupMembership/ModuleGrant models plus MembershipSource enum
(D-05), placed under TenantModuleActivation with German block comment
- Hand-SQL appended to the generated migration: partial unique index for
one default group per tenant (D-13), CHECK num_nonnulls xor-constraint
plus two partial unique indexes for ModuleGrant (D-04), and the D-06
backfill (Group -> GroupMembership -> ModuleGrant, each INSERT guarded
by WHERE NOT EXISTS for idempotent re-runs on `prisma migrate deploy`)
- apps/api/src/groups/migration-sql.spec.ts verifies the hand-SQL by
reading migration.sql directly, no DB required
- Verified against the local DB: default-group count matches tenant
count, membership/grant counts match existing users/active
activations, and the XOR constraint rejects a group+user-less insert