Add automatic push notifications for behind partners (escalating: 24h then every 3 days) and daily unpaired user reminders. Includes edge functions, migrations, notification preference toggles, and UI improvements to pairing and swipe screens.
14 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Important: Design Guidelines
Always read apps/mobile/DESIGN.md before making UI/UX changes to the mobile app. This file contains the design system, visual language, and styling patterns that must be followed for consistency. Agents and subagents should read this file when:
- Creating new screens or components
- Modifying existing UI elements
- Adding animations or visual effects
- Working with colors, typography, or spacing
Project Overview
Sauci is a couples relationship app that helps partners communicate better and grow closer together. Users swipe on thoughtful questions (yes/no/maybe), and when both partners answer positively, a "match" is created that unlocks a chat thread for that topic.
Commands
# Install dependencies (from root)
npm install
# Start all apps in development
npm run dev
# Run specific apps
cd apps/mobile && npm run dev # Expo mobile app
cd apps/admin && npm run dev # React Admin dashboard
# Type checking
npm run typecheck
# Linting
npm run lint
# Build all
npm run build
Mobile-specific commands
cd apps/mobile
npm run ios # Run on iOS simulator
npm run android # Run on Android emulator
npm run web # Run in browser
Supabase commands
supabase start # Start local Supabase
supabase db reset # Reset DB and run all migrations
supabase functions serve # Run edge functions locally
supabase migration new <name> # Create new migration
Supabase Environments
There are two Supabase projects configured via MCP servers:
| Environment | Project Ref | URL | MCP Server |
|---|---|---|---|
| Production | ckjcrkjpmhqhiucifukx |
https://ckjcrkjpmhqhiucifukx.supabase.co | sauci-prod |
| Non-Production | itbzhrvlgvdmzbnhzhyx |
https://itbzhrvlgvdmzbnhzhyx.supabase.co | sauci-non-prod |
When using Claude Code with MCP tools:
- Use
mcp__sauci-prod__*tools for production database operations - Use
mcp__sauci-non-prod__*tools for development/staging database operations - Always verify which environment you're targeting before running destructive operations
Database Migrations - CRITICAL
STOP: Read this entire section before making ANY database schema change.
The Golden Rule
ALL schema changes MUST go through local migration files. No exceptions.
Forbidden Actions
- NEVER call
apply_migrationvia MCP (eithermcp__sauci-prod__apply_migrationormcp__sauci-non-prod__apply_migration). This creates migration entries in the remote database with NO local file, which breakssupabase db pushin CI and causes deployment failures that are painful to fix. - NEVER run DDL statements (
CREATE,ALTER,DROP) viaexecute_sql. This changes the schema without any migration tracking at all. - NEVER manually INSERT into
supabase_migrations.schema_migrationsunless repairing a broken migration history (and only with explicit user approval).
Why This Matters
The CI/CD pipeline runs supabase db push --include-all which compares local migration files against the remote schema_migrations table. If the remote has migrations that don't exist locally, or local files exist without remote entries, the deployment will fail and block all subsequent deployments.
A single apply_migration call can break deployments for the entire project.
Correct Workflow for Schema Changes
# 1. Create migration file locally
supabase migration new <descriptive_name>
# 2. Edit the migration file in apps/supabase/migrations/
# 3. Make migrations idempotent where possible
# Use: CREATE OR REPLACE, IF NOT EXISTS, DROP IF EXISTS
# This prevents failures if re-run
# 4. Test locally
supabase db reset
# 5. Commit the migration file to git
# 6. Deploy happens automatically via CI on push to master
# OR deploy manually:
supabase link --project-ref <project_ref>
supabase db push
MCP Tools - What IS Allowed
- Reading data:
execute_sqlwith SELECT queries only - Exploring schema:
list_tables,list_migrations,list_extensions - Deploying edge functions:
deploy_edge_function - Generating types:
generate_typescript_types - Checking advisors:
get_advisors
If You Accidentally Used apply_migration
If a migration was applied via MCP, you must immediately:
- Retrieve the migration SQL from
schema_migrations.statements - Create a matching local file:
apps/supabase/migrations/<version>_<name>.sql - Commit and push the file so CI can reconcile
Architecture
Monorepo Structure (Turborepo)
apps/mobile- Expo React Native app (expo-router for navigation)apps/admin- React Admin dashboard for content managementapps/supabase- Database migrations and edge functionspackages/shared- Shared TypeScript types (@sauci/shared)
Mobile App Architecture
- Routing: Expo Router with file-based routing (
app/directory)(auth)/- Authentication screens(app)/- Main app screens (protected)
- State Management: Zustand stores in
src/store/index.tsuseAuthStore- User, couple, partner stateuseMatchStore- Matches and notificationsusePacksStore- Question packs and enabled packs per coupleuseMessageStore- Chat messages and unread countsuseSubscriptionStore- RevenueCat subscription state
- Backend: Supabase client in
src/lib/supabase.ts - UI Components: Glass-morphism themed components in
src/components/ui/
Data Model (Core Entities)
- Profile - User profile extending Supabase auth (has
couple_id) - Couple - Two users linked via invite code
- QuestionPack - Collection of questions (can be premium)
- Question - Individual question with intensity level (1-5)
- Response - User's answer (yes/no/maybe) to a question
- Match - Created when both partners answer positively (yes_yes, yes_maybe, maybe_maybe)
Edge Functions
Edge functions in apps/supabase/functions/ handle complex operations:
submit-response- Saves response, checks for matches, triggers notificationsmanage-couple- Join/leave couple operationsdelete-relationship- Deletes couple data (matches, messages, media) while keeping accountsdelete-account- Permanently deletes user account and all associated data (Apple App Store requirement)send-notification- Push notifications for matchessend-nudge-notification- Partner nudge notifications (rate limited to once per 12 hours)send-unpaired-reminder- Daily cron reminder for unpaired users to share invite codesend-catchup-reminder- Daily cron reminder for "behind" partners with escalating frequencyrevenuecat-webhook- Subscription webhook handlersync-subscription- Syncs RevenueCat subscription statusredeem-code- Redeems promotional codes for premium access (website only)
Edge Function Deployment Guidelines
When deploying edge functions via MCP tools (mcp__sauci-*__deploy_edge_function):
IMPORTANT: Always use verify_jwt: false for all edge functions. Supabase's built-in JWT verification causes 401 errors even with valid tokens. Instead, handle authentication manually in the function code using:
const { data: { user }, error: authError } = await supabase.auth.getUser(
authHeader.replace("Bearer ", "")
);
This approach:
- Gives better error messages
- Allows custom auth handling
- Avoids mysterious 401 failures
Current function settings (all should be verify_jwt: false):
| Function | verify_jwt | Auth Method |
|---|---|---|
manage-couple |
false | Manual via getUser() |
submit-response |
false | Manual via getUser() |
sync-subscription |
false | Manual via getUser() |
delete-relationship |
false | Manual via getUser() |
delete-account |
false | Manual via getUser() |
redeem-code |
false | Manual via getUser() |
revenuecat-webhook |
false | Webhook signature validation |
send-notification |
false | Internal trigger (no auth) |
send-message-notification |
false | Internal trigger (no auth) |
send-nudge-notification |
false | Manual via getUser() |
send-unpaired-reminder |
false | Cron trigger (no auth) |
send-catchup-reminder |
false | Cron trigger (no auth) |
Database Best Practices
- Always use
.maybeSingle()instead of.single()when the row might not exist..single()throws an error if 0 or >1 rows are returned. - Profile creation trigger: The
on_auth_user_createdtrigger automatically creates a profile when a user signs up. Never assume a profile exists without checking. - Check for null profiles in edge functions before performing operations.
Key Patterns
- Responses use UPSERT with
onConflict: "user_id,question_id" - Match detection happens in
submit-responseedge function after each response - Real-time subscriptions used for chat messages
- RevenueCat handles in-app purchases and subscription management
- RLS policies enforce couple-level data isolation
Redemption Code System
Promotional codes that grant users premium access. Codes can only be redeemed on the website, NOT in the mobile app.
Database Tables:
redemption_codes- Stores codes with max_uses, current_uses, expiration, active statuscode_redemptions- Tracks which users redeemed which codes
Components:
- Admin UI (
apps/admin/src/pages/RedemptionCodesPage.tsx) - Super admins can generate, manage, and view redemption codes - Edge Function (
apps/supabase/functions/redeem-code/) - Validates and processes code redemption (unauthenticated, accepts email + code) - Database Function (
redeem_code_by_email(p_email, p_code)) - Atomic redemption with validation (active, not expired, has uses remaining, user hasn't redeemed) - Web UI (
apps/web/app/redeem/page.tsx) - Public redemption page
Flow:
- Admin generates code in admin dashboard
- User visits
/redeemon website and enters email + code redeem-codeedge function callsredeem_code_by_email()database function- Database function looks up user by email, validates code, and sets
profiles.is_premium = true
Important: Do NOT add redemption code UI to the mobile app. Apple requires in-app purchases for premium features purchased within iOS apps.
Mobile UI - DO NOT CHANGE
Tab Bar Background
NEVER set backgroundColor: 'transparent' on the tab bar. Always use a semi-transparent solid color:
backgroundColor: 'rgba(18, 18, 18, 0.85)',
The BlurView in tabBarBackground is unreliable and can fail during re-renders/transitions, causing a fully transparent tab bar. The solid rgba background acts as a fallback while still allowing the blur effect on top.
Location: apps/mobile/app/(app)/_layout.tsx - tabBarStyle
Infrastructure as Code (Terraform)
Supabase project configuration is managed via Terraform in the terraform/ directory. This includes auth settings, OAuth providers, email templates, SMTP config, rate limits, and more.
Directory structure:
terraform/
├── modules/
│ └── supabase/ # Shared configuration module
│ ├── main.tf
│ ├── variables.tf
│ ├── outputs.tf
│ └── templates/ # Email templates
└── environments/
├── prod/ # Production (ckjcrkjpmhqhiucifukx)
└── non-prod/ # Non-production (itbzhrvlgvdmzbnhzhyx)
When to update Terraform:
- Changing auth settings (OAuth providers, JWT expiry, MFA settings)
- Modifying email templates or subjects
- Updating rate limits or security settings
- Changing SMTP configuration
- Modifying storage or API settings
Workflow (always test in non-prod first):
# 1. Apply to non-prod first
cd terraform/environments/non-prod
export TF_VAR_supabase_access_token=sbp_xxxxx
terraform plan
terraform apply
# 2. Verify changes work in non-prod
# 3. Apply to prod
cd terraform/environments/prod
terraform plan
terraform apply
Important: After making Terraform changes, remind the user to:
- Commit the updated
terraform/files - The GitHub Action will auto-apply on merge to master (non-prod first, then prod)
Do NOT use Terraform for:
- Database schema changes (use migrations)
- Edge function deployment (use CLI or MCP)
- Storage bucket creation (use migrations)
Edge Function Secrets
Edge functions use secrets stored in Supabase. These are synced from GitHub Secrets during CI/CD deployment.
Current secrets:
| Secret | Used By | Description |
|---|---|---|
REVENUECAT_WEBHOOK_SECRET |
revenuecat-webhook | Webhook signature validation |
REVENUECAT_API_KEY |
sync-subscription | RevenueCat API access |
REVENUECAT_ENTITLEMENT_ID |
sync-subscription | Entitlement identifier |
OPENROUTER_API_KEY |
classify-message | AI classification API |
ADMIN_PRIVATE_KEY_JWK |
admin-decrypt-* | E2EE admin decryption |
ADMIN_KEYS_JSON |
admin-decrypt-*, migrate-e2ee | Multiple admin keys |
DISCORD_WEBHOOK_URL |
send-discord-notification | Discord notifications |
Auto-provided by Supabase (don't set):
SUPABASE_URLSUPABASE_ANON_KEYSUPABASE_SERVICE_ROLE_KEY
When adding a new edge function that needs secrets:
- Add the secret to the edge function code:
Deno.env.get("SECRET_NAME") - Update
.github/workflows/deploy-supabase.ymlto include the new secret in the sync step - Update
apps/supabase/functions/.secrets.examplewith the new secret - Remind the user to add the secret to GitHub Secrets (Settings → Secrets → Actions)
- Update this table in CLAUDE.md
Local development:
cp apps/supabase/functions/.secrets.example apps/supabase/functions/.secrets
# Fill in values
supabase functions serve --env-file apps/supabase/functions/.secrets