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>
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()togetAdminClient() - 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
1. Created Deeplink Route
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
-
Update Backend Code
cd backend npm run build npm run start:prod -
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
-
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 -
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
-
Authentication Required
- User must be logged in to view shared characters
- Prevents anonymous abuse
- Tracks who accesses what (analytics)
-
Service Account Usage
- Backend uses service role key to bypass RLS
- Only exposed through authenticated endpoints
- No direct database access from frontend
-
Import Requirements
- Still requires authentication
- Costs 10 credits per import
- Prevents importing own characters
- Checks for duplicate imports
-
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_characterspolicy
Issue: "401 Unauthorized" when accessing shared endpoint
- Cause:
@Public()decorator not applied - Fix: Verify
PublicAuthGuardis being used and@Public()is on endpoint
Issue: App crashes when opening deeplink
- Cause: Missing mobile route file
- Fix: Ensure
mobile/app/share/character/[id].tsxexists
Issue: "Unmatched Route" error
- Cause: Expo Router can't find the route
- Fix: Restart Expo dev server with
npx expo start -c