feat(certifications): schema + RLS + permissions (data layer)

Migration 030 adds certifications (per-space cert types, optional
validity_months, is_active archive) and member_certifications (per-member
grants; expires_at snapshotted at grant time; soft revoke; partial unique
on one active grant per member+cert). RLS is additive and default-deny:
cert types readable by any space member, write-gated by
certifications.manage; grants readable by managers/granters (all) or the
member (own), written by certifications.grant, with no DELETE policy so
the history is immutable. No anonymous path.

Two new permission codes in the catalog (group Certifications):
certifications.manage and certifications.grant (the Instructor
capability, assignable to any role via the existing additive model).
Seeded to board and backfilled. Mirrored idempotently in schema.sql;
types and schema docs updated.
This commit is contained in:
Moheeb Zara
2026-05-16 18:51:33 -07:00
parent 04f1a31999
commit e439b1130f
6 changed files with 294 additions and 3 deletions
+12
View File
@@ -106,6 +106,18 @@ export type Database = {
Update: { id?: string; form_id?: string; space_id?: string; member_id?: string | null; submitter_email?: string | null; answers?: Json; form_snapshot?: Json; legal_text_snapshot?: string | null; form_version?: number; ip?: string | null; user_agent?: string | null; created_at?: string }
Relationships: []
}
certifications: {
Row: { id: string; space_id: string; name: string; description: string | null; validity_months: number | null; is_active: boolean; created_by: string | null; created_at: string; updated_at: string }
Insert: { id?: string; space_id: string; name: string; description?: string | null; validity_months?: number | null; is_active?: boolean; created_by?: string | null; created_at?: string; updated_at?: string }
Update: { id?: string; space_id?: string; name?: string; description?: string | null; validity_months?: number | null; is_active?: boolean; created_by?: string | null; created_at?: string; updated_at?: string }
Relationships: []
}
member_certifications: {
Row: { id: string; space_id: string; member_id: string; certification_id: string; granted_by: string | null; granted_at: string; expires_at: string | null; revoked_at: string | null; revoked_by: string | null; revoked_reason: string | null; note: string | null; created_at: string }
Insert: { id?: string; space_id: string; member_id: string; certification_id: string; granted_by?: string | null; granted_at?: string; expires_at?: string | null; revoked_at?: string | null; revoked_by?: string | null; revoked_reason?: string | null; note?: string | null; created_at?: string }
Update: { id?: string; space_id?: string; member_id?: string; certification_id?: string; granted_by?: string | null; granted_at?: string; expires_at?: string | null; revoked_at?: string | null; revoked_by?: string | null; revoked_reason?: string | null; note?: string | null; created_at?: string }
Relationships: []
}
activity_log: {
Row: {
action: string