Project
CARNALIFE
Private membership platforms live or die on trust: rigorous vetting has to coexist with real member discretion, and most admin tooling treats those as a tradeoff rather than solving for both. I built the full admin and vetting backend for a private membership platform, application review through ongoing safety enforcement, with every sensitive read and write enforced at the database level so member privacy holds even if the application code ever gets it wrong.

I built this as the sole engineer on a Next.js App Router and Supabase system covering the entire admin surface of a private membership platform: a multi-reviewer application queue with claim, recommend, approve, and deny actions and a full audit trail, Stripe Identity verification on intake, a reference and vouching system that gates approval, and a revocation workflow distinct from denial that blocks a revoked identity from quietly reapplying under different contact details. I architected the whole thing around Postgres Row Level Security rather than app-level role checks alone: every table's access rules live in the database itself, with anti-spoofing constraints (an actor can only ever attribute an action to their own account, enforced by the database, not just the UI) and column-level immutability triggers on audit tables so a completed action can't be silently edited later. The design decision I'm most proud of is the broadcast tool: rather than the obvious approach of putting every recipient in one email's To field, I send an individual API call per recipient, because on a discretion-first platform, one member seeing another member's address in an email header is a real privacy failure, not an edge case. I also built a KPI dashboard (application approval rate, referral share, safety-incident escalation rate) using Recharts, the first chart-based UI in the app, on top of an otherwise table-and-form-driven admin system.
Project README
CARNALIFE
A private membership platform for the kink, fetish, BDSM, and sex-positive lifestyle community. Built with Next.js (App Router) on the frontend and Supabase (Postgres, Auth, Storage, Row Level Security) on the backend.
Product/business context lives in docs/ (Brand Bible, Business Plan, Website Plan, Marketing Campaign, Legal Research). Engineering specs and implementation plans for each build phase live in docs/superpowers/specs/ and docs/superpowers/plans/. A running status summary of what's built vs. planned is in .claude/memory.md.
Repo layout
frontend/ Next.js app (this directory)
backend/ Supabase config, SQL migrations, RLS integration tests
docs/ Product/business docs and engineering specs & plans
Note on the Next.js version: this repo pins a Next.js release with breaking changes relative to older docs/training data. Before writing code that touches routing, middleware, or server/client boundaries, check
frontend/node_modules/next/dist/docs/for the actual current behavior.
Prerequisites
- Node.js
- Docker (for local Supabase)
- Supabase CLI
Getting started
1. Start the local Supabase stack (from backend/):
cd backend
supabase start
This applies every migration in backend/supabase/migrations/ to a fresh local Postgres instance and prints your local API URL and keys. Studio, Mailpit (catches outgoing email locally), and the Postgres connection are all available from the URLs it prints.
2. Configure environment variables (frontend/.env.local, gitignored):
SUPABASE_URL=http://127.0.0.1:54321
SUPABASE_ANON_KEY=<from `supabase status`>
SUPABASE_SERVICE_ROLE_KEY=<from `supabase status`>
STRIPE_SECRET_KEY=<Stripe test key>
STRIPE_IDENTITY_WEBHOOK_SECRET=<Stripe CLI `stripe listen` webhook secret>
RESEND_API_KEY=<optional — omitted, transactional emails log to the console instead of sending>
There is no root .env or .env.example checked in (secrets never belong in git); ask whoever set up the project for real values, or use your own Stripe test-mode keys.
3. Run the dev server (from frontend/):
npm install
npm run dev
Open http://localhost:3000.
Testing
Frontend (Vitest + Testing Library, Supabase client mocked):
cd frontend
npm test # run once
npm run test:watch
Backend (Vitest, run against a real local Postgres via RLS — not mocked; requires supabase start first):
cd backend
npm test
Both suites are expected to be green on master at all times.
Linting & type-checking
cd frontend
npm run lint
npx tsc --noEmit
What's here
Public site (frontend/app/(public)/): homepage, About, sub-brand pages (Revelry, Knowledge live; others coming-soon), consent and vetting-criteria policy pages, the membership application funnel (/apply, with Stripe Identity verification and a self-service reference-confirmation flow), and an application-status page.
Admin (frontend/app/admin/(protected)/, gated by frontend/proxy.ts + lib/auth/require-staff.ts / require-admin.ts, navigable via a categorized sidebar in the shared layout): a founder-authored blog CMS, the vetting reviewer queue (claim/recommend/approve/deny with a full audit log), members management (revoke/lift with reinstatement lockout enforced at application intake), a referral tracker with an admin-configurable per-referral credit system, and a settings page for that credit amount.
Auth (frontend/app/forgot-password/, frontend/app/reset-password/, frontend/app/api/forgot-password/, frontend/app/api/reset-password/): a shared password-recovery flow serving both /admin/login and /apply/login, built on Supabase Auth's generateLink/exchangeCodeForSession rather than a custom token system. Requires <origin>/api/reset-password/callback to be on the Supabase project's Redirect URLs allow-list before it works (see .claude/memory.md).
Backend (backend/supabase/migrations/): one SQL file per schema change, applied in order. RLS is the enforcement layer throughout — the app's Supabase clients rely on policies, not just application-code checks, to gate access by role (member / reviewer / admin).
Key conventions
- Every admin-facing route handler calls a
verifyOrigincheck before anything else, then gates on the session's role via Supabase RLS (requireStaff/requireAdmin), never trusting client-supplied identity. - State-changing actions that matter for audit/compliance write to an append-only log table (
application_reviews,member_revocations,member_revocation_attempts) rather than only updating a status column in place. - Transactional email (
frontend/lib/email/) falls back to a console log whenRESEND_API_KEYisn't set, so the full application/vetting flow works end-to-end in local dev with no email provider configured. - No browser-side Supabase client exists anywhere in this codebase. Every Supabase call happens server-side, in Route Handlers or Server Components (
lib/supabase/server.tsfor service-role,lib/supabase/session-server.tsfor cookie-bound sessions); client components only everfetch()this app's own API routes.
Project Gallery
This album serves as the gallery for CARNALIFE.


