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
| ID | Status | Finding | Area | Fix |
|---|---|---|---|---|
| RLS-01 | P0 | Cross-Tenant Row Leak via Direct REST API | Data Isolation | 1.5 hours |
| PAY-01 | P0 | Webhook Handler Lacks Idempotency Guard | Stripe & Billing | 2.0 hours |
| OPS-01 | P0 | Point-in-Time Recovery (PITR) Never Drill-Tested | Disaster Recovery | 2.5 hours |
| SEC-02 | P1 | SECURITY DEFINER Function Lacks search_path Hardening | Privilege Escalation | 30 mins |
| MIG-01 | P1 | Destructive Schema Migration Without Tested Rollback Script | Migrations | 1.0 hour |
| MON-01 | P1 | Edge Functions Lack Runtime Exception Capture | Monitoring | 45 mins |
| AUTH-01 | PASS | JWT Token Refresh & Password Hashing | Password & Tokens | — |
| DB-01 | PASS | Foreign Key Cascades & Strict Typing | Foreign Keys | — |
| PAY-02 | PASS | Server-Side Price ID Enforcement | Price 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 drillAfter
# 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 runbookSEC-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.
- Apply tenant isolation RLS policies — replace open policies with an app_metadata tenant check (1.5h).
- Create a processed_events webhook table — make Stripe fulfilment idempotent against retries (2.0h).
- 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.