This guide walks you through testing the database persistence implementation step-by-step.
Before testing, you need:
- ✅ Supabase project created
- ✅ Schema deployed (ran schema.sql)
- ✅ Clerk organization created
- ✅ At least one Clerk user created
- ✅ Backend running (local or deployed)
-- Check if all tables exist
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
ORDER BY table_name;- audit_logs
- audit_usage
- ingestion_logs
- organizations
- policies
- rate_limit_buckets
- subscriptions
- users
- Copy entire schema.sql again
- Go to Supabase → SQL Editor
- Paste and run the entire script
- Re-run the verification query above
In Supabase SQL Editor, check if user exists:
SELECT * FROM users LIMIT 5;If empty, continue to Option B.
- Go to Clerk Dashboard
- Click "Users" → "Create new user"
- Enter email:
test@example.com - Click "Create user"
- Wait 2-3 seconds for webhook to fire
-- Check if user synced
SELECT id, clerk_user_id, email, name, organization_id, created_at
FROM users
ORDER BY created_at DESC
LIMIT 1;id: <UUID>
clerk_user_id: user_xxx
email: test@example.com
name: <name from Clerk>
organization_id: NULL (if not in an org yet)
created_at: <recent timestamp>
- User appears in Supabase users table
- Clerk user ID correctly mapped
- Email matches Clerk
- Timestamp is recent (within last few seconds)
- Go to Clerk Dashboard → Organizations
- Click "Create organization"
- Enter name:
Test Org - Click "Create"
- Wait 2-3 seconds
-- Check if organization synced
SELECT id, clerk_org_id, name, slug, created_at
FROM organizations
ORDER BY created_at DESC
LIMIT 1;id: <UUID>
clerk_org_id: org_xxx
name: Test Org
slug: test-org
created_at: <recent timestamp>
- Organization appears in Supabase
- Clerk org ID correctly mapped
- Name matches
- Timestamp is recent
# Get your actual values first:
# ORG_UUID = from step 3 (organizations.id)
# USER_ID = from step 2 (users.clerk_user_id)
curl -X POST https://sheetbrain-ai.vercel.app/api/billing \
-H "Content-Type: application/json" \
-H "x-user-id: YOUR_CLERK_USER_ID" \
-H "x-user-org: YOUR_CLERK_ORG_ID" \
-d '{"plan": "free"}'{
"plan": "free",
"status": "active",
"customerId": "cus_xxx",
"subscriptionId": null,
"quotaLimit": 10,
"usageThisMonth": 0
}-- Check subscriptions table
SELECT id, organization_id, stripe_customer_id, plan, status, created_at
FROM subscriptions
ORDER BY created_at DESC
LIMIT 1;plan: free
status: active
created_at: <recent timestamp>
- Subscription created in Supabase
- Plan is "free"
- Status is "active"
- Created timestamp is recent
curl -X POST https://sheetbrain-ai.vercel.app/api/ingest \
-H "Content-Type: application/json" \
-H "x-user-id: YOUR_CLERK_USER_ID" \
-H "x-user-org: YOUR_CLERK_ORG_ID" \
-d '{
"title": "Test Formula Policy",
"content": "Never use VOLATILE functions like TODAY() or NOW() in static reports. Always wrap calculations with error handling using IFERROR.",
"department": "Finance",
"tags": ["formulas", "best-practices"]
}'{
"success": true,
"policy": {
"id": "uuid",
"title": "Test Formula Policy",
"content": "...",
"createdAt": "2026-01-03T..."
}
}-- Check policies
SELECT id, organization_id, title, content, created_at
FROM policies
ORDER BY created_at DESC
LIMIT 1;
-- Check ingestion logs
SELECT id, organization_id, policy_id, document_size, success, created_at
FROM ingestion_logs
ORDER BY created_at DESC
LIMIT 1;Policies table:
title: Test Formula Policy
content: Never use VOLATILE functions...
created_at: <recent timestamp>
Ingestion_logs table:
success: true
document_size: <length of content>
created_at: <recent timestamp>
- Policy appears in policies table
- Ingestion log created
- Document size recorded correctly
- Success = true
curl -X POST https://sheetbrain-ai.vercel.app/api/audit \
-H "Content-Type: application/json" \
-H "x-user-id: YOUR_CLERK_USER_ID" \
-H "x-user-org: YOUR_CLERK_ORG_ID" \
-d '{
"formulas": [
"=SUM(A1:A10)",
"=VLOOKUP(B2, Sheet1!A:B, 2, FALSE)",
"=IF(ISNUMBER(C5), C5*2, 0)"
],
"context": {
"sheetName": "Test Sheet",
"range": "A1:C10",
"organization": "Test Org"
}
}'{
"success": true,
"audits": [
{
"cellAddress": "A1",
"compliant": true,
"risk": "low",
"issues": [],
"recommendations": [...]
},
...
],
"count": 3,
"compliant": 3,
"duration": 1234
}-- Check audit logs
SELECT id, organization_id, user_id, formula_count, compliant_count, issues_found, duration_ms, created_at
FROM audit_logs
ORDER BY created_at DESC
LIMIT 1;
-- Check usage tracking
SELECT organization_id, month_year, count
FROM audit_usage
ORDER BY created_at DESC
LIMIT 1;
-- Check rate limit buckets
SELECT organization_id, request_count, window_reset_at
FROM rate_limit_buckets
ORDER BY updated_at DESC
LIMIT 1;Audit_logs:
formula_count: 3
compliant_count: 3
issues_found: 0
duration_ms: <positive number>
created_at: <recent timestamp>
Audit_usage:
month_year: 2026-01
count: 1
Rate_limit_buckets:
request_count: 1 (or higher if made multiple requests)
window_reset_at: <future timestamp>
- Audit log created in database
- Formula count correct (3)
- Usage recorded in audit_usage
- Rate limit bucket created/updated
- Timestamps all recent
# Run this 5+ times quickly to exceed the default 100 req/min limit
for i in {1..5}; do
curl -X POST https://sheetbrain-ai.vercel.app/api/audit \
-H "Content-Type: application/json" \
-H "x-user-id: YOUR_CLERK_USER_ID" \
-H "x-user-org: YOUR_CLERK_ORG_ID" \
-d '{"formulas": ["=SUM(A1:A10)"], "context": {"sheetName": "Test"}}'
echo "Request $i"
done- First requests succeed (status 200)
- After ~100 requests in 60 seconds, get rate limit error (status 429)
-- Check rate limit bucket
SELECT organization_id, request_count, window_reset_at
FROM rate_limit_buckets
WHERE window_reset_at > NOW();request_count: 100+ (or configured limit)
window_reset_at: <time in the future>
- Requests work normally
- Rate limit bucket tracking requests
- Window reset time is in the future
- Go to Clerk Dashboard → Webhooks
- Add endpoint:
https://sheetbrain-ai.vercel.app/api/auth/webhook - Copy webhook secret to
.env.localasCLERK_WEBHOOK_SECRET - Subscribe to:
- user.created
- user.updated
- user.deleted
- organization.created
- organization.updated
- organization.deleted
- organizationMembership.created
- organizationMembership.updated
In Clerk Dashboard → Webhooks → click on your endpoint:
- You should see recent deliveries (green checkmarks)
- Click on each to see the payload
In Supabase Dashboard → Logs:
Filter by: api.auth.webhook
Look for: POST /api/auth/webhook
Expected: Status 200 (received)
- Webhook endpoint configured in Clerk
- Recent deliveries show green checkmarks
- Supabase logs show 200 responses
- Users sync when created in Clerk
-- Use the stats function
SELECT * FROM get_org_stats('YOUR_ORG_UUID'::UUID);Replace YOUR_ORG_UUID with the actual UUID from the organizations table.
total_users: 1+
total_policies: 1+
total_audits: 1+
audits_this_month: 1+
plan: free
subscription_status: active
- Function returns correct counts
- Audits_this_month matches audit_usage
- Plan matches subscriptions table
Complete all tests above and verify:
- All 8 tables created
- All 9 indexes created
- All 4 functions created
- Users sync from Clerk to Supabase
- Organizations sync from Clerk to Supabase
- Memberships update user's organization_id
- Subscriptions created and persisted
- Usage tracked monthly in audit_usage
- Quota checking works
- Policies saved to database
- Ingestion logs track uploads
- Default policies seeded
- Audit logs saved to database
- All audit data recorded (formulas, issues, duration)
- RAG usage tracked
- Rate limit buckets created
- Requests counted per window
- 429 response when limit exceeded
- Clerk sends webhooks
- Backend receives and processes
- Data syncs to Supabase
Solution:
- Check
.env.localhasCLERK_WEBHOOK_SECRETset - Verify webhook endpoint in Clerk is correct
- Check Clerk webhook delivery logs
- Restart backend
- Create new user in Clerk to trigger webhook
Solution:
- Ensure organization exists in database
- Check x-user-org header is being sent
- Verify audit endpoint is being called
- Check backend logs for errors
Solution:
- Verify rate_limit_buckets table exists
- Check RATE_LIMIT_ENABLED is not 'false'
- Verify x-user-org header sent with requests
- Check database function increment_rate_limit exists
Solution:
- Go to Supabase → SQL Editor
- Run:
SELECT * FROM pg_proc WHERE proname LIKE 'increment%'; - If empty, re-run schema.sql
- Check for SQL syntax errors in functions
✅ All tests passed if you see:
- All tables have data
- Clerk events sync to Supabase automatically
- Audit logs record formula audits
- Rate limiting tracks requests
- Usage is counted monthly
- ✅ Verify all tests pass
- Monitor Supabase dashboard for data
- Check application logs for errors
- Set up automated backups in Supabase
- Configure Row-Level Security (RLS) policies
- Add monitoring alerts
Ready to test? Start with Step 1 and work your way through! 🚀