skills/realtime-rls-architecture/SKILL.md
RLS policy architecture for Supabase Realtime subscriptions — access control layers, JWT validation logic for visitor/user tokens, verified production policy templates. Use when writing or reviewing RLS policies for tables used with Realtime. Do NOT use for debugging active subscription failures — load realtime-rls-debugging instead.
npx skillsauth add bkinsey808/songshare-effect realtime-rls-architectureInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Requires: file-read. No terminal or network access needed.
Depends on: authentication-system/SKILL.md — load when token-claim behavior or JWT structure is in scope. realtime-rls-debugging/SKILL.md — load when the issue is active breakage (messages not arriving).
Preconditions:
supabase/migrations/ to understand the table shape.Clarifying questions:
song_public — covering both visitor and user tokens. Let me know if owner-only access was intended."Output format:
CREATE POLICY statement(s) in a fenced SQL code block.Error handling:
realtime-rls-debugging skill.| Layer | Mechanism | Purpose | | ----- | --------- | ------- | | Database RLS | Row-level policies enforce owner/admin/participant access | Primary protection | | API | Service role with business-logic validation | Secondary check | | Frontend | Hide edit/delete UI for non-owners | UX safety | | Realtime filtering | RLS SELECT policies filter per-subscriber | Real-time safety |
Key principle: Realtime respects RLS. Subscriptions succeed regardless, but message delivery is silently filtered — a client without SELECT permission receives nothing, no error.
The API uses the service role (bypasses RLS) for writes. Realtime then broadcasts, and RLS filters each subscriber independently.
This project uses two token types. Any SELECT policy on a public-readable table must handle both:
(
-- Visitor token: app_metadata.visitor_id is set
(((auth.jwt() -> 'app_metadata'::text) ->> 'visitor_id'::text) IS NOT NULL)
OR
-- User token: app_metadata.user.user_id is set
((((auth.jwt() -> 'app_metadata'::text) -> 'user'::text) ->> 'user_id'::text) IS NOT NULL)
)
| Token type | visitor_id | user.user_id | Result |
| ---------- | ------------ | -------------- | ------ |
| Visitor (anonymous) | ✅ set | null | TRUE OR FALSE → access ✅ |
| User (signed in) | null | ✅ set | FALSE OR TRUE → access ✅ |
| Invalid/malformed | null | null | FALSE OR FALSE → blocked ❌ |
See authentication-system skill for full token generation details.
event_public)CREATE POLICY "Allow read access to public events"
ON public.event_public FOR SELECT TO authenticated
USING (
is_public = true
AND (
(((auth.jwt() -> 'app_metadata'::text) ->> 'visitor_id'::text) IS NOT NULL)
OR
((((auth.jwt() -> 'app_metadata'::text) -> 'user'::text) ->> 'user_id'::text) IS NOT NULL)
)
);
CREATE POLICY "Allow owner to read own event_public"
ON public.event_public FOR SELECT TO authenticated
USING (
owner_id = (((auth.jwt() -> 'app_metadata'::text) -> 'user'::text) ->> 'user_id'::text)::uuid
);
CREATE POLICY "Allow participant to read event_public"
ON public.event_public FOR SELECT TO authenticated
USING (
EXISTS (
SELECT 1 FROM event_user eu
WHERE eu.event_id = event_public.event_id
AND eu.user_id = (((auth.jwt() -> 'app_metadata') -> 'user' ->> 'user_id')::uuid)
AND eu.status IN ('invited', 'joined', 'left')
)
);
CREATE POLICY "Allow owner to update own event_public"
ON public.event_public FOR UPDATE TO authenticated
USING (
owner_id = (((auth.jwt() -> 'app_metadata'::text) -> 'user'::text) ->> 'user_id'::text)::uuid
)
WITH CHECK (
owner_id = (((auth.jwt() -> 'app_metadata'::text) -> 'user'::text) ->> 'user_id'::text)::uuid
);
CREATE POLICY "Allow event admins to update event_public"
ON public.event_public FOR UPDATE TO authenticated
USING (
EXISTS (
SELECT 1 FROM event_user eu
WHERE eu.event_id = event_public.event_id
AND eu.user_id = (((auth.jwt() -> 'app_metadata') -> 'user' ->> 'user_id')::uuid)
AND eu.role IN ('event_admin', 'event_playlist_admin')
AND eu.status = 'joined'
)
);
CREATE POLICY "Allow owner to delete own event_public"
ON public.event_public FOR DELETE TO authenticated
USING (
owner_id = (((auth.jwt() -> 'app_metadata'::text) -> 'user'::text) ->> 'user_id'::text)::uuid
);
WITH CHECK on UPDATE must also pass for Realtime to deliver the post-update row.Input: "Write RLS policies for song_public so both visitors and authenticated users can read public songs"
Expected: Agent outputs a fenced SQL CREATE POLICY for SELECT using the dual JWT template (visitor_id IS NOT NULL OR user.user_id IS NOT NULL), scoped to is_public = true. Follows with a bullet list: visitors and users both receive Realtime broadcasts for public songs; users without a valid token are silently filtered out.
Input: "Why is my visitor seeing events in the UI but the Realtime subscription delivers nothing?"
Expected: Agent identifies this as an active debugging task, defers to realtime-rls-debugging skill for root cause workflow. May note that a missing or malformed SELECT policy is the most common cause (see Key insight #1).
Input: "Add owner-only UPDATE access to the song_public table"
Expected: Agent outputs the UPDATE policy template using owner_id = user.user_id::uuid, includes both USING and WITH CHECK clauses, notes that WITH CHECK is required for Realtime to deliver the post-update row.
supabase/migrations/20260220000011_re_enable_rls_on_event_public.sqldocs/ai/rules.md.WITH CHECK on UPDATE policies — Realtime requires it to deliver the updated row.tools
Zustand state management patterns for this project — store creation, selectors, Immer middleware, async actions with loading states, devtools, persist, and testing. Use when authoring or editing Zustand stores (use*Store files) or components that subscribe to stores. Do NOT use for React component structure or TypeScript-only utilities.
testing
How to write, update, or split skill files in this repo. Use when creating a new SKILL.md, updating an existing one, or deciding whether to put content in a skill vs. docs/.
development
Complete guide for testing React hooks — renderHook, Documentation by Harness, installStore, fixtures, subscription patterns, lint/compiler traps, and pre-completion checklist. Read docs/testing/unit-test-hook-best-practices.md for the full reference.
development
Vitest unit test authoring for this repo — setup, mocking, API handler testing, and common pitfalls for non-hook code. Use when the user asks to add, update, fix, or review unit tests for utilities, components, API handlers, or scripts. Do NOT use for React hook tests — load unit-test-hook-best-practices instead.