ScaleRocket/Web

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:

HeaderValuePurpose
X-Frame-OptionsDENYPrevents clickjacking
X-Content-Type-OptionsnosniffPrevents MIME type sniffing
Referrer-Policystrict-origin-when-cross-originControls referrer information
X-DNS-Prefetch-ControlonEnables DNS prefetching for performance
Strict-Transport-Securitymax-age=31536000; includeSubDomainsForces HTTPS for 1 year
Permissions-Policycamera=(), microphone=(), geolocation=()Disables unused browser APIs
Content-Security-PolicySee belowPrevents 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.io to script-src and connect-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.com

Note: The stripe-webhook function 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

  1. Never expose the service role key in apps/web or apps/app. It's only for apps/ops and Edge Functions.
  2. Always enable RLS on new tables. A table without RLS allows anyone with the anon key to read all rows.
  3. Always validate JWT in protected Edge Functions using the getUser() helper.
  4. Always verify Stripe webhook signatures to prevent forged events.
  5. 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.

On this page