managarten/maerchenzauber/apps/backend/RLS_IMPLEMENTATION_GUIDE.md
Till-JS e7f5f942f3 chore: initial commit - consolidate 4 projects into monorepo
Projects included:
- maerchenzauber (NestJS backend + Expo mobile + SvelteKit web + Astro landing)
- manacore (Expo mobile + SvelteKit web + Astro landing)
- manadeck (NestJS backend + Expo mobile + SvelteKit web)
- memoro (Expo mobile + SvelteKit web + Astro landing)

This commit preserves the current state before monorepo restructuring.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-22 23:38:24 +01:00

6.2 KiB

RLS Implementation Guide for Maerchenzauber Storyteller

Overview

This document describes the Row Level Security (RLS) implementation for the Maerchenzauber Storyteller project, based on proven patterns from the Memoro project. The implementation provides secure, user-based access control for both database tables and storage buckets.

Implemented Changes

1. Core Authentication Functions

We've created four essential authentication helper functions:

current_user_id()

  • Purpose: Extracts user identity from JWT claims
  • Returns: User ID as text
  • Usage: Primary function for identifying the current user in RLS policies

auth_is_admin()

  • Purpose: Checks if the current user has admin privileges
  • Returns: Boolean (true if admin, false otherwise)
  • Usage: Used in policies that require admin access (creators, errors tables)

current_user_uuid()

  • Purpose: Returns the current user's ID as UUID
  • Returns: UUID
  • Usage: Compatibility function when UUID type is needed

user_owns_resource(resource_user_id text)

  • Purpose: Helper function to check resource ownership
  • Returns: Boolean
  • Usage: Simplifies ownership checks in policies

2. Table RLS Policies

Characters Table

  • View: Users can only see their own characters
  • Insert: Users can only create characters with their user_id
  • Update: Users can only update their own characters
  • Delete: Users can only delete their own characters
  • Service Role: Full access for backend operations

Stories Table

  • View: Users can only see their own stories
  • Insert: Users can only create stories with their user_id
  • Update: Users can only update their own stories
  • Delete: Users can only delete their own stories
  • Service Role: Full access for backend operations

Creators Table

  • View: All authenticated users can view creators
  • Insert/Update/Delete: Only admins can modify creators
  • Service Role: Full access for backend operations

Errors Table

  • View: Only admins can view errors
  • Insert: Authenticated users can insert their own errors (for logging)
  • Update/Delete: Only admins can modify errors
  • Service Role: Full access for backend operations

3. Storage RLS Policies

storyteller-images bucket (public)

  • Structure: {user_id}/stories/{story_id}/ or {user_id}/characters/{character_id}/
  • View: Anyone can view images (public bucket)
  • Upload/Update/Delete: Users can only manage files in their own folder (first folder must match their user_id)
  • Admin Access: Admins can view and delete any file

user-uploads bucket (private)

  • Structure: {user_id}/{filename}
  • View/Upload/Update/Delete: Users can only access their own folder
  • Admin Access: Admins can view and delete any file

4. Migration Files Created

  1. auth-functions.sql: Core authentication functions
  2. update-rls-policies.sql: Updated RLS policies using new functions
  3. storage-rls-policies.sql: Storage bucket policies
  4. cleanup-duplicate-policies.sql: Removed duplicate policies
  5. test-rls-policies.sql: Comprehensive test queries

Usage in Application Code

TypeScript/JavaScript Example

// File upload with proper path structure
const uploadFile = async (file: File, userId: string, storyId: string) => {
  const filePath = `${userId}/stories/${storyId}/${Date.now()}_${file.name}`;
  
  const { data, error } = await supabase.storage
    .from('storyteller-images')
    .upload(filePath, file);
    
  if (error) throw error;
  return data;
};

// Fetching user's own data (RLS automatically filters)
const getUserCharacters = async () => {
  const { data, error } = await supabase
    .from('characters')
    .select('*');
    
  // Only returns characters where user_id matches current user
  return data;
};

Admin Operations

// Check if user is admin
const checkAdminStatus = async () => {
  const { data, error } = await supabase
    .rpc('auth_is_admin');
    
  return data; // true or false
};

// Admin creating a creator (requires admin role)
const createCreator = async (creatorData: CreatorInput) => {
  const { data, error } = await supabase
    .from('creators')
    .insert(creatorData);
    
  // Will fail if user is not admin
  return data;
};

Security Best Practices

  1. Always use auth functions: Use current_user_id() instead of direct JWT manipulation
  2. Test policies thoroughly: Use the test-rls-policies.sql file to verify access
  3. Service role key protection: Never expose service role key in client code
  4. Consistent user identification: Always store user_id as text for compatibility
  5. Storage path structure: Follow the defined folder structure for storage

Troubleshooting

Common Issues

  1. Access Denied Errors

    • Verify user is authenticated
    • Check if user_id matches the resource owner
    • Ensure RLS is enabled on the table
  2. Admin Functions Not Working

    • Verify user has admin flag in users table
    • Check JWT claims include proper app_access data
    • Ensure auth_is_admin() function is working correctly
  3. Storage Upload Failures

    • Verify file path follows the required structure
    • Check bucket exists and RLS is enabled
    • Ensure user is authenticated

Debug Queries

-- Check current user
SELECT current_user_id(), auth_is_admin();

-- View active policies
SELECT * FROM pg_policies WHERE tablename = 'your_table';

-- Test specific access
SET request.jwt.claim.sub = 'test-user-id';
SELECT * FROM characters;

Maintenance

Adding New Tables

When adding new tables with RLS:

  1. Enable RLS: ALTER TABLE new_table ENABLE ROW LEVEL SECURITY;
  2. Create policies using auth functions
  3. Add service role policy
  4. Test all CRUD operations
  5. Document the policies

Updating Policies

  1. Always drop old policy before creating new one
  2. Use migrations for version control
  3. Test changes thoroughly
  4. Update documentation

References