-
Notifications
You must be signed in to change notification settings - Fork 0
Database
ChuckleChest uses Supabase (Postgres) with Row-Level Security (RLS) for all authorization.
| Command | Description |
|---|---|
hobnob db:start |
Start local Supabase stack |
hobnob db:reset |
Reset local DB (applies migrations + seed) |
hobnob db:diff |
Generate migration from schema changes |
hobnob db:test |
Run pgTAP tests |
chests -- A family/group container (top-level tenant)
├── gems -- A funny quote/moment (auto-numbered per chest)
│ ├── lines -- Individual lines of dialogue in a gem
│ └── gem_share_tokens
├── collections -- Curated groups of gems
│ └── collection_share_tokens
├── people -- Family members who say funny things
│ └── person_avatar_urls
├── user_roles -- Maps auth users to roles within a chest
├── invitations -- Pending invites to join a chest
└── role_permissions -- Which role can do what (seeded, not user-editable)
users -- Mirrors auth.users (created by trigger)
Every data table has a chest_id column. A chest is the multi-tenant boundary —
users only see data in chests they belong to.
Authorization is enforced entirely in Postgres via RLS policies. The app never checks permissions in Dart code.
Three roles exist per chest, defined as app_role enum:
| Role | Purpose |
|---|---|
owner |
Full control — manage members, delete chest |
collaborator |
Create and edit content, share gems |
viewer |
Read-only access |
-
JWT claims —
custom_access_token_hookruns on every token refresh. It readsuser_rolesand embeds achestsclaim into the JWT:{ "chests": { "<chest-id>": { "name": "Smith Family", "role": "owner" } } } -
authorize()function — Every RLS policy callsauthorize(permission, chest_id). It extracts the user's role for that chest from the JWT claim and checks it againstrole_permissions. -
RLS policies — Each table has policies like:
CREATE POLICY "Allow authorized select access" ON "public"."gems" FOR SELECT TO authenticated USING (authorize('gems.select', chest_id));
-
role_permissionstable — Seeded viaseed.sql. Maps each role to its allowed operations. This table is not editable by users.
| Resource | Owner | Collaborator | Viewer |
|---|---|---|---|
| Chests | CRUD | CR | CR |
| Gems | CRUD | CRUD | R |
| Lines | CRUD | CRUD | R |
| People | CRUD | CRUD | R |
| Person avatars | CRUD | CRUD | R |
| Collections | CRUD | CRUD | R |
| Share tokens | CRD | CRD | — |
| Gem likes | CRD* | CRD* | CRD* |
| Invitations | CRUD | — | — |
| User roles | RU | — | — |
* Gem likes: users can only create/read/delete their own like — RLS policies
also require user_id = auth.uid().
Note: owners cannot modify their own user_roles row (policy enforces
user_id <> auth.uid()).
-
Sign up —
on_auth_user_insertedtrigger creates ausersrow -
Create chest —
on_chest_insertedtrigger assignsownerrole and seeds a welcome gem -
Invite member — Owner inserts into
invitationswith email and role -
Accept invite —
accept_invitation()moves the invitation intouser_rolesand deletes it - Share publicly — Share tokens allow unauthenticated access to individual gems or collections via dedicated fetch functions
- Make schema changes locally
- Run
hobnob db:diff - Never modify a migration already on
main— add new - Never push to the live database — CI handles on merge
hobnob db:diff includes functions you didn't change — investigate before
committing.
seed.sql contains role permissions the app depends on. Do not delete or modify
without understanding the permission system.
See also: Authentication · Storage · Architecture