feat(forms): schema + RLS for custom forms and waivers (Phase 1)

Adds the forms and form_submissions tables behind a custom form/waiver
builder. Phase 1 is schema + RLS only; server actions, builder UI, the
public /f/[slug] page, onboarding integration, and retro-linking follow
in later phases.

- scripts/026_forms.sql (idempotent), mirrored as schema.sql Section 17
- forms: globally-unique slug, kind (form/waiver), visibility
  (public_anon/public_auth/members), status, jsonb field schema,
  legal_text, version
- form_submissions: per-row snapshot of schema/legal-text/version for
  immutable waiver records, plus ip/user_agent for e-sign defensibility
- RLS additive + default-deny: forms write gated by forms.manage via
  user_has_permission; form_submissions has no write policy, so it is
  service-client-only and immutable; anon gets no grant on forms
- new forms.manage permission seeded to board + backfilled
- types/database.ts and DATABASE_SCHEMA/DB_SCHEMA_MAP/ARCHITECTURE/HANDOFF
  updated
This commit is contained in:
Moheeb Zara
2026-05-16 14:38:28 -07:00
parent 3ace1afa17
commit 3a2d3ca13f
8 changed files with 302 additions and 5 deletions
+12
View File
@@ -94,6 +94,18 @@ export type Database = {
Update: { member_id?: string; custom_role_id?: string; assigned_at?: string }
Relationships: []
}
forms: {
Row: { id: string; space_id: string; slug: string; title: string; description: string | null; kind: string; visibility: string; status: string; schema: Json; legal_text: string | null; version: number; created_by: string | null; created_at: string; updated_at: string }
Insert: { id?: string; space_id: string; slug: string; title: string; description?: string | null; kind?: string; visibility?: string; status?: string; schema?: Json; legal_text?: string | null; version?: number; created_by?: string | null; created_at?: string; updated_at?: string }
Update: { id?: string; space_id?: string; slug?: string; title?: string; description?: string | null; kind?: string; visibility?: string; status?: string; schema?: Json; legal_text?: string | null; version?: number; created_by?: string | null; created_at?: string; updated_at?: string }
Relationships: []
}
form_submissions: {
Row: { 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 }
Insert: { 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 }
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: []
}
activity_log: {
Row: {
action: string