DOC-ID: VG-2026-SA09-SYNTHETIC

Launch Gate Audit — Sample Report

The deliverable a founder receives before risking live users or ad spend, shown in full.

Product
EduPremium SaaS — multi-tenant B2B academy platform (React, TanStack, Supabase, Stripe)
Audit type
Launch Gate Audit — one week
Audit date
2026-09
Prepared by
vibeguard (Jaeil Lee)
Report status
Sample

Sample — written against a fictional product, not a client's app.

Verdict

No-Go for public launch as-is.

The core workflow is well-architected, but 3 P0 isolation, billing and recovery defects must be fixed before public onboarding. Do not rebuild the codebase: all 3 can be closed in a 48-hour stabilization pass, about six engineering hours, without touching the existing UI.

Findings

Every finding, by severity

IDStatusFindingAreaFix
RLS-01P0Cross-Tenant Row Leak via Direct REST APIData Isolation1.5 hours
PAY-01P0Webhook Handler Lacks Idempotency GuardStripe & Billing2.0 hours
OPS-01P0Point-in-Time Recovery (PITR) Never Drill-TestedDisaster Recovery2.5 hours
SEC-02P1SECURITY DEFINER Function Lacks search_path HardeningPrivilege Escalation30 mins
MIG-01P1Destructive Schema Migration Without Tested Rollback ScriptMigrations1.0 hour
MON-01P1Edge Functions Lack Runtime Exception CaptureMonitoring45 mins
AUTH-01PASSJWT Token Refresh & Password HashingPassword & Tokens—
DB-01PASSForeign Key Cascades & Strict TypingForeign Keys—
PAY-02PASSServer-Side Price ID EnforcementPrice Validation—

What each finding means

RLS-01 P0 · Cross-Tenant Row Leak via Direct REST API

Diagnosis

The Next.js UI filters courses and student records by tenant_id, but the PostgreSQL RLS policy on academy_records uses USING (true), so any signed-in user can read every row — tenant membership is never checked.

Business impact

Any authenticated student from Academy A can read billing, grades, and contact records of Academy B by issuing a direct fetch to the Supabase PostgREST endpoint.

Verification — Two-session access check

// Session A (User A, Academy #102)
const { data } = await supabase
  .from('academy_records')
  .select('*')
  .eq('tenant_id', 999); // Academy B (#999)

// Result: 200 OK — 48 private student dossiers returned.

Before

-- VULNERABLE: Only checks if user is logged in
CREATE POLICY "Allow select for users" ON academy_records
FOR SELECT TO authenticated
USING (true);

After

-- SECURED: Enforces strict tenant boundary via JWT claims
CREATE POLICY "Strict tenant isolation" ON academy_records
FOR SELECT TO authenticated
USING (
  tenant_id = (auth.jwt() -> 'app_metadata' ->> 'tenant_id')::bigint
);

PAY-01 P0 · Webhook Handler Lacks Idempotency Guard

Diagnosis

The checkout.session.completed webhook processes student enrollment and subscription credits without recording processed event IDs in an idempotency table.

Business impact

When Stripe automatically retries webhook delivery upon network jitter, duplicate course credits and duplicate welcome emails are triggered.

Verification — Webhook replay

// Simulated Stripe Webhook Retry (Same Event ID: evt_3N9x...)
POST /api/webhooks/stripe (Delivery #1) -> 200 OK (Credits granted: +100)
POST /api/webhooks/stripe (Delivery #2) -> 200 OK (Credits granted: +100)
// Total credits: 200 (Expected: 100)

Before

// VULNERABLE: Direct DB update without event tracking
if (event.type === 'checkout.session.completed') {
  await grantCredits(session.customer, session.amount_total);
  return res.json({ received: true });
}

After

// SECURED: Atomic insert into processed_events table
const { error } = await supabase
  .from('processed_webhook_events')
  .insert({ event_id: event.id, processed_at: new Date().toISOString() });

if (error && error.code === '23505') {
  // 23505 = Unique violation -> already processed
  return res.json({ received: true, deduplicated: true });
}

await grantCredits(session.customer, session.amount_total);

OPS-01 P0 · Point-in-Time Recovery (PITR) Never Drill-Tested

Diagnosis

Automated backups are enabled in Supabase settings, but WAL archiving and point-in-time branch restoration have never been tested against a staging database.

Business impact

In the event of a botched migration or malicious table drop during Beta, estimated Recovery Time Objective (RTO) is undefined and data loss risk is high.

Verification — Observation

Observation: Staging environment has no automated restore script. Recovery runbook missing.

Before

# Current State: Default cloud dashboard toggle with no verified restore drill

After

# Recovery drill, on a staging branch:
# 1. Restore to a point in time before a test migration
# 2. Time the restore; compare row counts with the source
# 3. Record the measured RTO in the runbook

SEC-02 P1 · SECURITY DEFINER Function Lacks search_path Hardening

Diagnosis

A database function used to calculate monthly payouts runs with SECURITY DEFINER privileges but omits SET search_path = public.

Business impact

Possibility of search_path hijacking if a malicious schema is injected by a compromised role.

MIG-01 P1 · Destructive Schema Migration Without Tested Rollback Script

Diagnosis

Migration 20260815_restructure_plans.sql drops the legacy tier column without a backward-compatible transition phase.

Business impact

If the new deployment fails in production, rolling back the application will crash because the old column was dropped.

MON-01 P1 · Edge Functions Lack Runtime Exception Capture

Diagnosis

Sentry is configured on the Next.js frontend and Node server, but Supabase Edge Functions fail silently on unhandled promise rejections.

Business impact

Async webhook and background billing failures will leave no traces in error tracking.

What passed

  • AUTH-01 · JWT Token Refresh & Password Hashing — Bcrypt hash rounds and Supabase JWT refresh rotation interval are correctly configured.
  • DB-01 · Foreign Key Cascades & Strict Typing — All relational constraints, UUID validation, and deletion cascades are strictly modeled.
  • PAY-02 · Server-Side Price ID Enforcement — Stripe Price IDs are mapped on the server; client cannot submit arbitrary billing amounts.

48-hour P0 plan

These 3 patches move the verdict from No-Go to Go for a closed beta. The 3 P1 findings go into the next sprint.

  1. Apply tenant isolation RLS policies — replace open policies with an app_metadata tenant check (1.5h).
  2. Create a processed_events webhook table — make Stripe fulfilment idempotent against retries (2.0h).
  3. Run a staging PITR recovery drill — verify the branch-restore runbook and record the RTO (2.5h).

Founder Summary

The decision

Run the two-day patch sprint, then open the closed beta as soon as RLS is verified.

What I’d fix before the next milestone

The 3 P0s in section 05. Nothing else changes the launch decision.

What can wait

The P1s go into the next sprint. A full GraphQL migration, microservice decomposition and secondary audit logging can wait until past $10k MRR — do not spend budget on them today.

About your current developer

The frontend UX and database schema show disciplined product thinking. The defects are the boundary oversights typical of fast AI-assisted prototyping, not fundamental design flaws.

What I looked at

Auth and RLS, schema and migrations, payments, and ops and recovery — the areas in section 02. Not a penetration test and not a compliance certification.

If you want the detail

Sections 02 and 03 of this report, for the person who will fix them.

Want this review before your launch?

The Launch Gate Audit is $1,200 fixed and takes a week. I verify every gate myself, and you receive a prioritized risk table, quick wins for the blockers, and a 7, 14 or 30-day next-sprint plan.