managarten/maerchenzauber/apps/backend/docs/CHARACTER_SHARING_RLS.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

5.5 KiB

Character Sharing with Service Account

Overview

Character sharing uses authenticated requests where users must be logged in, but the backend uses the service account (service role key) to bypass RLS when fetching shared characters.

Architecture

Security Model:

  • User must be authenticated to view shared characters
  • Backend uses service account to bypass RLS
  • No special RLS policies needed
  • All character data remains protected by default RLS

Benefits:

  • Know who is accessing shared characters (analytics, abuse prevention)
  • No public endpoints exposing database directly
  • Simpler RLS policy management
  • Users must be logged in to import (already required for credits)

Required RLS Policies

User-Owned Characters Policy

Standard RLS policy - users can only read their own characters:

-- Policy name: "Users can read their own characters"
CREATE POLICY "users_read_own_characters"
ON characters
FOR SELECT
USING (auth.uid() = user_id);

Note: No additional policies needed! The backend service account bypasses RLS entirely.

Testing

Test 1: Verify Authenticated Access to Shared Character

# Test authenticated user accessing a shared character
curl -X GET "http://localhost:3002/character/shared/{characterId}" \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json"

# Expected: 200 OK with character data (any character ID)

Test 2: Verify Unauthenticated Access is Blocked

# Test without auth token
curl -X GET "http://localhost:3002/character/shared/{characterId}" \
  -H "Content-Type: application/json"

# Expected: 401 Unauthorized

Test 3: Verify User-Owned Characters Still Work

# Test authenticated user accessing their own characters
curl -X GET "http://localhost:3002/character" \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json"

# Expected: 200 OK with list of user's characters

Backend Changes Made

1. Added Admin Client to SupabaseProvider

File: backend/src/supabase/supabase.provider.ts

  • Added getAdminClient() method
  • Uses MAERCHENZAUBER_SUPABASE_SERVICE_ROLE_KEY
  • Bypasses all RLS policies

2. Updated getSharedCharacter

File: backend/src/core/services/supabase-jsonb-auth.service.ts

  • Changed from getClient() to getAdminClient()
  • Uses service account for database access
  • User still must be authenticated to call endpoint

3. CharacterController

File: backend/src/character/character.controller.ts

  • Kept @UseGuards(AuthGuard) - requires authentication
  • Added @CurrentUser() to track who accesses shared characters

Mobile App Changes

File: mobile/app/share/character/[id].tsx

  • Handles maerchenzauber://share/character/{id} deeplinks
  • Redirects to character preview page
  • Added error handling to prevent crashes

2. Improved Error Handling

  • Added try/catch blocks
  • User-friendly error messages
  • Graceful fallback to home screen on errors

Deployment Steps

  1. Update Backend Code

    cd backend
    npm run build
    npm run start:prod
    
  2. Update RLS Policies in Supabase Dashboard

    • Go to Supabase Dashboard > Authentication > Policies
    • Add the "public_read_shared_characters" policy
    • Verify existing user policies remain active
  3. Deploy Backend to Cloud Run

    cd backend
    gcloud builds submit --tag gcr.io/PROJECT_ID/storyteller-backend
    gcloud run deploy storyteller-backend --image gcr.io/PROJECT_ID/storyteller-backend
    
  4. Build and Deploy Mobile App

    cd mobile
    eas build --platform ios --profile production
    eas submit --platform ios
    

Verification Checklist

  • Backend compiles without TypeScript errors
  • Public endpoint is accessible without authentication
  • Authenticated users can still access their own characters
  • RLS policy allows reading shared characters
  • RLS policy blocks reading private characters
  • Deeplink opens app successfully
  • Character preview loads without crashing
  • Error handling works for invalid character IDs
  • Import functionality still requires authentication (10 credits)

Security Considerations

  1. Authentication Required

    • User must be logged in to view shared characters
    • Prevents anonymous abuse
    • Tracks who accesses what (analytics)
  2. Service Account Usage

    • Backend uses service role key to bypass RLS
    • Only exposed through authenticated endpoints
    • No direct database access from frontend
  3. Import Requirements

    • Still requires authentication
    • Costs 10 credits per import
    • Prevents importing own characters
    • Checks for duplicate imports
  4. RLS Still Protects Data

    • Users can only modify their own characters
    • Default RLS policies remain unchanged
    • Service account only used for read operations on shared endpoint

Troubleshooting

Issue: "Character not found or not available for sharing"

  • Cause: RLS policy not configured correctly
  • Fix: Add the public_read_shared_characters policy

Issue: "401 Unauthorized" when accessing shared endpoint

  • Cause: @Public() decorator not applied
  • Fix: Verify PublicAuthGuard is being used and @Public() is on endpoint
  • Cause: Missing mobile route file
  • Fix: Ensure mobile/app/share/character/[id].tsx exists

Issue: "Unmatched Route" error

  • Cause: Expo Router can't find the route
  • Fix: Restart Expo dev server with npx expo start -c