Garrett Smith
  1. Report
  2. 3 Engagements
  3. E-08
E-08Demo · fictional client

Taking a Lovable-pattern Supabase app to production

The app is a customer portal for Tallgrass Precision Components, a fictional machine shop.

TypeWorking demo for Tallgrass Precision Components, a fictional manufacturer
StatusLive demo
ServicesC-5 Production hardening for AI-built apps
Stack
  • Vite + React (JavaScript)
  • Supabase (Postgres 17 with Row-Level Security, Storage, Edge Functions, GoTrue)
  • Supabase CLI
  • Node test runner
  • Playwright
  • Claude Code (claude -p, Sonnet) for the first pass

The problem

Supabase is on my skills list, but most "rescue my AI-built app" jobs I see on Upwork are not the Next.js server-backed shape I'd already written up. They're Lovable or Bolt output: a Vite or Next frontend talking straight to Supabase from the browser, with the anon key in the bundle and the database handling auth and tenant isolation through Row-Level Security. When that RLS is off, permissive, or bypassed by a leaked service key, every customer's data is one PostgREST request away from anyone else's. I wanted a case specific to that shape, with proof instead of a checklist.

What I built

The app is a customer portal for Tallgrass Precision Components, a fictional machine shop. Customers log in, see their own orders, download their quality certificates, and open RMAs. Staff see everything.

I generated the first version with headless Claude Code (claude -p, Sonnet) on 2026-10-12, prompting it as a non-technical founder asking specifically for the Lovable pattern. Its first pass was careful: real RLS policies, a private storage bucket, the service key kept out of .env with a warning not to put it there. That isn't what most rescue jobs look like, so I reverted the before/ app to the Supabase-specific flaw classes I actually see — RLS disabled, RLS "on" but using (true), a profile row whose tenant and role the user can edit, a leaked service_role key, a public storage bucket, an unchecked SECURITY DEFINER function, a missing WITH CHECK, an edge function with open CORS and no auth, and a tenant invite code anyone could read — and documented each one I injected.

How it works

claude -p first pass

(recorded)

before/

13 flaws injected,

documented

Local Supabase stack

app_before / app_after

same Postgres instance

13 exploits + 8 functional tests

tests/run.mjs

RLS lint

scripts/rls-lint.mjs

results.json

audit/RESULTS.md

claude -p first pass

(recorded)

before/

13 flaws injected,

documented

Local Supabase stack

app_before / app_after

same Postgres instance

13 exploits + 8 functional tests

tests/run.mjs

RLS lint

scripts/rls-lint.mjs

results.json

audit/RESULTS.md

Figure 1. How it works Open full size

before/ and after/ run against two schemas in the same local Postgres instance, through the same PostgREST endpoint and the same anon key, so both can be attacked live without restarting anything. The hardened app scopes every policy to tenant_id = current_tenant_id() or is_staff(), where current_tenant_id() is a SECURITY DEFINER helper that reads the caller's own profile row — never a value the client supplies.

Security and control

  • RLS enabled on every tenant table, with real per-tenant policies, not using (true).
  • No update path for a customer's own tenant_id or role — set once, server-side, at signup.
  • No service_role key anywhere in the client; it exists only as an edge function secret.
  • Storage bucket private, with policies scoped by tenant path segment.
  • The SECURITY DEFINER RPC now runs SECURITY INVOKER plus an explicit staff check.
  • The edge function requires a verified session, is rate-limited, and never echoes raw Postgres errors.
  • A plain-SQL RLS lint runs alongside the exploits and documents its own blind spot: it can't tell a real policy from a permissive one, which is exactly why the exploits exist too.

Results

Measured by tests/run.mjs on 2026-10-13:

  • 13 findings: 4 Critical, 5 High, 3 Medium, 1 Low.
  • 13/13 exploits succeed against the before app. 0/13 succeed against the after app.
  • 8/8 functional tests pass against the after app.
  • An unauthenticated request with just the anon key read every RMA across both demo tenants in before; blocked in after.
  • A customer's own session read all 6 orders across 2 companies in before; only their own 3 in after.
  • The service_role key was found verbatim in the before client bundle; absent from after.

Stack

Vite + React (JavaScript), Supabase (Postgres 17 with Row-Level Security, Storage, Edge Functions, GoTrue), Supabase CLI, Node test runner, Playwright, Claude Code (claude -p, Sonnet) for the first pass.

What I'd do for your company

If you have a Supabase app built with Lovable, Bolt, Claude, or Cursor and want it in front of real customers, I would: read every RLS policy, not just whether RLS is on; write an exploit for each gap so you can see it's real; fix them without rewriting what works; and hand back the fixed database, the test suite, and a plain-language report.

Tallgrass Precision Components is a fictional company used for this demo.

Exhibits

Screenshots are real renders of the running demo. Data shown belongs to the fictional Tallgrass Precision Components.

  • After orders 390
    Exhibit AAfter orders 390
  • After orders 1440
    Exhibit BAfter orders 1440
  • After rmas 1440
    Exhibit CAfter rmas 1440
  • App login 1440
    Exhibit DApp login 1440
  • Before certs 1440
    Exhibit EBefore certs 1440
  • Before orders 1440
    Exhibit FBefore orders 1440

Want something like this on your own data?

Send me the details on Upwork: what the AI should reach, who will use it and who signs off. I will reply with how I would build it and what it would touch.

Hire me on Upwork
Prepared by Garrett SmithAI Integration EngineerContracts and messages through Upwork
REVIEWED & TESTEDAPPROVED FOR HANDOFFG. SMITH · ACCOUNTABLE