SpinAI persists all of its state in a PostgreSQL database hosted on Supabase. Five tables collaborate to track team members, their weekly facilitator assignments, slide preparation templates, swap audit logs, and AI news items surfaced during meetings. Every table has Row Level Security enabled, and access is enforced at the application layer through a PIN-gated JWT rather than through Supabase Auth roles.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/fmoraga01/SpinAI/llms.txt
Use this file to discover all available pages before exploring further.
Row Level Security is enabled on all five tables, but the policies are permissive (
anon full access). Real access control happens at the app layer: the Next.js middleware validates a signed JWT on every request, and the Supabase client uses the anon key only after that gate has been passed. This design is intentional for a small internal team tool — see PIN Gate Authentication for details.members
Stores each team member who participates in the weekly meeting facilitator roulette.
| Column | Type | Constraints | Default |
|---|---|---|---|
id | uuid | PRIMARY KEY | gen_random_uuid() |
name | text | NOT NULL | — |
email | text | — | NULL |
active | boolean | NOT NULL | true |
created_at | timestamptz | NOT NULL | now() |
active flag controls whether a member is included in the roulette spin. Deactivating rather than deleting a member keeps their historical assignment records intact.
assignments
Each row represents a single facilitator slot — one Friday date paired with the member scheduled to run that meeting.
| Column | Type | Constraints | Default |
|---|---|---|---|
id | uuid | PRIMARY KEY | gen_random_uuid() |
member_id | uuid | FK → members(id) ON DELETE SET NULL | NULL |
member_name | text | — | NULL |
date | date | NOT NULL | — |
created_at | timestamptz | NOT NULL | now() |
member_name is denormalised alongside member_id so that past assignment history is preserved even after a member is renamed or removed. The ON DELETE SET NULL constraint means that deleting a member leaves the assignment slot in the schedule as unassigned rather than removing it entirely — the slot can be filled later by re-running the roulette.
assignments_date_idx speeds up date-ordered fetches of the schedule; assignments_member_id_idx accelerates member-scoped queries such as finding all slots for a given person.
templates
Holds the slide preparation content for each assignment — agenda items, key talking points, presenter notes, and visual theming preferences.
| Column | Type | Constraints | Default |
|---|---|---|---|
id | uuid | PRIMARY KEY | gen_random_uuid() |
assignment_id | uuid | NOT NULL, UNIQUE, FK → assignments(id) ON DELETE CASCADE | — |
member_id | uuid | FK → members(id) ON DELETE SET NULL | NULL |
member_name | text | — | NULL |
title | text | — | NULL |
agenda | jsonb | — | '[]'::jsonb |
key_points | jsonb | — | '[]'::jsonb |
notes | text | — | NULL |
theme | text | — | 'default' |
font | text | — | 'sans' |
size | text | — | 'md' |
updated_at | timestamptz | NOT NULL | now() |
assignment_id carries a UNIQUE constraint — there is exactly one template per assignment. The ON DELETE CASCADE means that removing an assignment automatically removes its template. agenda and key_points were originally text[] arrays and were converted to jsonb in migration 20260702180000 to align with the production schema.
agenda and key_points were originally created as text[] in 20260702000000_esquema_inicial.sql and converted to jsonb by 20260702180000_agenda_key_points_a_jsonb.sql. The SQL above shows the current schema state after that migration. The migration is idempotent — it checks the current column type before altering, so it is safe to run on a database where the columns are already jsonb.assignment_logs
An append-only audit trail of every manual swap between two facilitator slots. The roulette spin itself does not create a log entry — only intentional swaps performed in the UI do.
| Column | Type | Constraints | Default |
|---|---|---|---|
id | uuid | PRIMARY KEY | gen_random_uuid() |
member_a_name | text | — | NULL |
member_b_name | text | — | NULL |
date_a | date | — | NULL |
date_b | date | — | NULL |
created_at | timestamptz | NOT NULL | now() |
assignment_logs_created_at_idx sorts entries descending so the most recent swaps are returned first without a sort pass.
news_items
Caches AI-topic news articles fetched from RSS feeds by the background cron route (/api/cron/refresh-news). Items are surfaced in the meeting slides for the facilitator to review before the session.
| Column | Type | Constraints | Default |
|---|---|---|---|
id | uuid | PRIMARY KEY | gen_random_uuid() |
source | text | NOT NULL | — |
title | text | NOT NULL | — |
url | text | NOT NULL, UNIQUE | — |
summary | text | — | NULL |
image_url | text | — | NULL |
published_at | timestamptz | NOT NULL | — |
fetched_at | timestamptz | NOT NULL | now() |
UNIQUE constraint on url enables upsert-based deduplication: re-fetching the same article updates it in place rather than creating a duplicate row. The cron job uses on conflict (url) do update to refresh summary, image_url, and fetched_at for articles that already exist.
news_items_published_at_idx returns articles in reverse-chronological order efficiently, which is how the news section always queries them.
Row Level Security
RLS is enabled on all five tables. Because SpinAI uses the Supabase anon key directly (there is no per-user Supabase Auth session), each table carries a single permissive policy that allows the anon role full read/write access. The actual security boundary is the PIN-gated JWT enforced by Next.js middleware before any Supabase request is ever made.Full migration SQL
View full migration SQL
View full migration SQL
20260702000000_esquema_inicial.sql — Creates the initial four tables and enables RLS.20260702180000_agenda_key_points_a_jsonb.sql — Converts agenda and key_points from text[] to jsonb.20260702190000_crear_news_items.sql — Creates the news_items table.