Security Overview
Security headers, CORS, input validation, error handling, and production checklist.
Overview
ScaleRocket follows security best practices across all layers -- database (RLS), API (Edge Functions), and client (auth guards). This page covers the security architecture and what to verify before going to production.
Security Architecture
Client (Browser)
├── Auth Guard (route protection)
├── Supabase client (anon key, JWT)
└── HTTPS only
│
Edge Functions (Server)
├── JWT validation
├── CORS headers
├── Input validation
├── Stripe signature verification
└── Service role (bypasses RLS)
│
Database (Supabase)
├── Row Level Security (RLS)
├── Triggers (server-side logic)
└── Functions (SECURITY DEFINER)Security Headers
The marketing site (apps/web) includes comprehensive security headers in next.config.ts:
| Header | Value | Purpose |
|---|---|---|
X-Frame-Options | DENY | Prevents clickjacking |
X-Content-Type-Options | nosniff | Prevents MIME type sniffing |
Referrer-Policy | strict-origin-when-cross-origin | Controls referrer information |
X-DNS-Prefetch-Control | on | Enables DNS prefetching for performance |
Strict-Transport-Security | max-age=31536000; includeSubDomains | Forces HTTPS for 1 year |
Permissions-Policy | camera=(), microphone=(), geolocation=() | Disables unused browser APIs |
Content-Security-Policy | See below | Prevents XSS and injection attacks |
Content Security Policy (CSP)
The CSP header is the most important security header. It tells the browser which sources of content are allowed:
// apps/web/next.config.ts
{
key: "Content-Security-Policy",
value: [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://js.stripe.com https://*.supabase.co",
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
"font-src 'self' https://fonts.gstatic.com",
"img-src 'self' data: blob: https://*.supabase.co https://*.stripe.com",
"connect-src 'self' https://*.supabase.co https://api.stripe.com https://*.resend.com",
"frame-src https://js.stripe.com https://*.supabase.co",
"object-src 'none'",
"base-uri 'self'",
"form-action 'self'",
"frame-ancestors 'none'",
].join("; "),
}Customizing CSP: If you add third-party scripts (analytics, chat widgets), add their domains to the appropriate directive. For example, to allow Plausible analytics, add
https://plausible.iotoscript-srcandconnect-src.
For the Vite apps (apps/app, apps/ops), configure these headers in your hosting platform (Vercel vercel.json or headers configuration).
CORS Configuration
Edge Functions use the APP_URL environment variable for CORS, restricting requests to your app domain only:
// supabase/functions/_shared/supabase.ts
const allowedOrigin = Deno.env.get("APP_URL") || "http://localhost:5173";
export const corsHeaders = {
"Access-Control-Allow-Origin": allowedOrigin,
"Access-Control-Allow-Headers":
"authorization, x-client-info, apikey, content-type",
};Set APP_URL in your Supabase secrets for production:
npx supabase secrets set APP_URL=https://app.yourdomain.comNote: The
stripe-webhookfunction uses a separate CORS handler since Stripe webhooks don't send an origin header. Webhook security relies on signature verification, not CORS.
Input Validation
Validate all input in Edge Functions before processing:
Deno.serve(async (req) => {
const { priceId, mode } = await req.json();
// Validate required fields
if (!priceId || typeof priceId !== "string") {
return new Response(
JSON.stringify({ error: "Invalid priceId" }),
{ status: 400 }
);
}
// Validate against allowed values
if (!["subscription", "payment"].includes(mode)) {
return new Response(
JSON.stringify({ error: "Invalid mode" }),
{ status: 400 }
);
}
// Proceed with validated data
});Error Handling
Never expose internal errors to the client:
try {
// Function logic
} catch (error) {
// Log the full error server-side
console.error("Function failed:", error);
// Return a generic message to the client
return new Response(
JSON.stringify({ error: "An unexpected error occurred" }),
{ status: 500, headers: corsHeaders }
);
}Key Security Rules
- Never expose the service role key in
apps/weborapps/app. It's only forapps/opsand Edge Functions. - Always enable RLS on new tables. A table without RLS allows anyone with the anon key to read all rows.
- Always validate JWT in protected Edge Functions using the
getUser()helper. - Always verify Stripe webhook signatures to prevent forged events.
- Use SECURITY DEFINER sparingly. Functions with this flag run as the owner (superuser), bypassing RLS.
Production Security Checklist
Before deploying to production:
- RLS enabled on every table with appropriate policies
- CORS restricted to your actual domains (not
*) - Service role key only in admin app and Edge Function secrets
- Stripe webhook secret set in Supabase secrets
- Security headers configured on all apps
- OAuth redirect URLs restricted to your domains in Supabase dashboard
- Email templates customized (no default Supabase branding)
- Admin whitelist updated with real admin emails
- Rate limiting enabled in Supabase dashboard
- Database backups enabled (Supabase Pro plan)
- Stripe test mode switched to live mode
- Environment variables use production values (no test keys)
Done reading? Mark this page as complete.