Common Errors
504 on OAuth Callback
Symptom: The /auth/callback route times out with a 504 error after OAuth sign-in.
Cause: The default Vercel function timeout is too short for OAuth token exchange.
Fix: Set maxDuration on the callback route:
// app/auth/callback/route.ts
export const maxDuration = 30;
This requires a Vercel Pro plan. The Hobby plan caps at 10 seconds.
BigInt Serialization Error
Symptom: TypeError: Do not know how to serialize a BigInt when returning data from API routes or Server Actions.
Cause: PostgreSQL bigint columns are returned as JavaScript BigInt, which JSON.stringify() cannot serialize.
Fix: Cast to int in your SQL queries:
-- Bad: returns BigInt
SELECT count(*) FROM tenant_controls WHERE tenant_id = $1;
-- Good: returns number
SELECT count(*)::int FROM tenant_controls WHERE tenant_id = $1;
Always use ::int casts on aggregate functions (count, sum) in queries that return data to the client. This is a recurring issue across the codebase.
RLS Permission Denied
Symptom: Queries return empty results or throw permission denied errors, but the data exists in the database.
Cause: The authenticated user does not have a valid membership for the target tenant.
Debugging steps:
-- Check if the user has a tenant membership
SELECT * FROM tenant_memberships
WHERE user_id = 'user-uuid' AND tenant_id = 'tenant-uuid';
-- Check if the user has access through a partner
SELECT ptl.tenant_id
FROM partner_tenant_links ptl
JOIN partner_memberships pm ON pm.partner_id = ptl.partner_id
WHERE pm.user_id = 'user-uuid';
-- Check the active_tenant_id cookie value in the browser
Unique Constraint on Reconnect
Symptom: duplicate key value violates unique constraint when reconnecting an integration.
Cause: The integration_connections table has a unique constraint on (tenant_id, provider_id). Reconnecting without removing the old connection triggers a conflict.
Fix: Use the upsert pattern:
await supabase.from('integration_connections').upsert(
{
tenant_id,
provider_id,
access_token_vault_id: vaultId,
status: 'active',
connected_at: new Date().toISOString(),
},
{ onConflict: 'tenant_id,provider_id' }
);
Missing Columns in Views
Symptom: column "xyz" does not exist when querying a view after a migration.
Cause: Views are snapshots of queries. If the underlying table changes, the view must be recreated.
Fix: Drop and recreate the view in a migration:
DROP VIEW IF EXISTS v_controls_overview;
CREATE OR REPLACE VIEW v_controls_overview AS
SELECT
tc.*,
-- Add the new column here
...
FROM tenant_controls tc
...;
Cookie Deadlock in Route Handlers
Symptom: Route handler hangs or produces inconsistent cookie behavior.
Cause: Writing to cookieStore (from next/headers) inside a Route Handler while also modifying response.cookies creates a deadlock.
Fix: Always write to response.cookies, not cookieStore:
// Bad: causes deadlock
const cookieStore = cookies();
cookieStore.set('active_tenant_id', tenantId);
// Good: write to response
const response = NextResponse.json({ success: true });
response.cookies.set('active_tenant_id', tenantId, {
path: '/',
httpOnly: true,
secure: true,
});
return response;
Edge Function 503
Symptom: Edge Function returns 503 Service Unavailable.
Cause: Often caused by redeclaring supabaseUrl inside the function instead of importing from _shared/supabase-client.ts.
Fix: Import shared client configuration:
// Bad: redeclaring causes 503
const supabaseUrl = Deno.env.get('SUPABASE_URL')!;
// Good: import from shared module
import { supabaseUrl, supabaseClient } from '../_shared/supabase-client.ts';
This is a known issue documented in project memory. Never redeclare supabaseUrl in sync functions.
Supabase Logs
View Edge Function logs:
supabase functions logs sync-azure-ad --project-ref hcyyegiialkkjcdxpfat
Database Query Logs
Check integration_sync_log for sync issues:
SELECT * FROM integration_sync_log
WHERE connection_id = 'uuid'
ORDER BY created_at DESC
LIMIT 10;
Vercel Function Logs
View Next.js API route and Server Action logs in the Vercel dashboard under the Functions tab, or via CLI:
pg_cron Job History
SELECT jobname, start_time, end_time, status, return_message
FROM cron.job_run_details
ORDER BY start_time DESC
LIMIT 20;
When debugging RLS issues, temporarily use getSupabaseServerAdminClient() to confirm the data exists, then switch back to the authenticated client to test policies. Never leave admin client usage in user-facing code.