WORK INDEX
BOOK A CALLEMAIL ME
OWN PRODUCT  ·  IN PRODUCTION  ·  2026

The database isthe application.

PRODUCT
Stencil
ROLE
Everything
TENANCY
Row level security
STACK
Next.js · Supabase
DATA
Postgres
Stencil request pipeline
01  THE PROBLEM

A tattoo studio is several businesses sharing a calendar.

Artists take their own enquiries, quote their own work, keep their own clients and their own income, and share a studio, a diary and a front desk. Software that treats the studio as one account gets the calendar right and the money wrong. Software that treats each artist as an island cannot show the studio a week.

So the question is not what the screens look like. It is who is allowed to see which row, and whether that answer is enforced somewhere a missing filter cannot undo.

The constraint I set: no query written in the app should be able to return another studio's row, even if I write it badly.
02  ARCHITECTURE

No API tier, on purpose.

A Next.js app talking to Postgres through Supabase, with no service of my own in between. That only works because the authorisation lives in the database: without row level security this shape would be one forgotten filter away from a leak, and I would have had to build the tier back. This is the opposite call to ClassPortal, where the domain was wide enough that a NestJS API earned its keep.

FIG. 02.1 / SYSTEM
CLIENT
next.js
SUPABASE
typed client
RLS
35 policies
POSTGRES
source of truth
AUTHORISATION IS A POLICY ON THE TABLE, NOT A CLAUSE IN A QUERY
13 TABLES, ALL WITH RLS ON, 35 POLICIES BETWEEN THEM
03  DATA MODEL

Thirteen tables, and the database says no.

Every table carries its own policies, its own checks and its own foreign keys with the deletion behaviour spelled out. A booking outlives the enquiry it came from; a client outlives the payment that referenced them.

inquiries
Seven states. The funnel is the product, so the states earn their own type rather than living as booleans.
bookings
Its own five-state life, linked to the enquiry that produced it but surviving that enquiry being deleted.
clients
Deduplicated on a generated, digits-only copy of the phone number, indexed per artist.
payments
Integer cents, checked positive, with the method recorded as cash, transfer, card or other. Money is recorded here, not processed.
reminders
One row per intended message with a scheduled time and a pending, sent or failed status, so a message that never went is visible rather than assumed.
04  STACK MANIFEST
CLIENT
Next.js · React · Tailwind · shadcn
FORMS
react-hook-form · Zod
DATA
Postgres · Supabase · RLS
SERVICES
Resend · Vercel · next-intl
Need multi-tenant data that cannot leak?