INTERACTIVE DEMO — SYNTHETIC DATA
DENAVUM
A three-space personal operating system whose security boundary is a database policy. This page explains it and does not simulate it — because faking a database policy would argue the opposite of the thing being claimed.
Every record on this page is fabricated. No production system, customer, employee or credential is involved.
The production code behind this demo is my own work — architecture, implementation.
WHY THERE IS NO SIMULATION ON THIS PAGE
This project has no live simulation, because the thing worth demonstrating is a database policy — and faking a database policy is not on.
The obvious demo writes itself: issue the same query as two different users, watch one row set shrink, caption it “the database decided that, not the interface”. It would take an afternoon and it would be the least honest thing on this site. There is no Postgres in your browser, so the two users would be a variable, the policy would be an if statement, and the filtering would happen in exactly the place the project spent fifteen migrations moving it out of.
A UI-level check impersonating a database-level one does not illustrate Row Level Security. It demonstrates the failure mode Row Level Security exists to remove — on a project whose README opens by listing what it has not verified.
So: the architecture is here, explorable. The policies are here, as the migration files write them. The unverified list is here, first rather than last. And two parts of the system that port honestly are genuinely running further down.
- migration files
- 15MIGRATION FILES
- tables defined in them
- 47TABLES DEFINED IN THEM
- tests, passing
- 91TESTS, PASSING
- of them run against Postgres
- 0OF THEM RUN AGAINST POSTGRES
WHAT IS VERIFIED, AND WHAT IS NOT
Reproduced from the project’s README, where it is the first section rather than a footnote. Its own opening line: “Being precise about this matters more than sounding confident.”
VERIFIED ON THIS MACHINE
| CHECK | RESULT |
|---|---|
| npm run typecheck | passes, zero errors (TypeScript strict + noUncheckedIndexedAccess) |
| npm run lint | passes, zero errors (6 informational React Compiler warnings) |
| npm run test | 91 tests pass across 5 files |
| npm run build | production build succeeds, 28 routes compiled |
NOT VERIFIED — IT NEEDS A LIVE SUPABASE PROJECT
- The SQL migrations have not been executed against a real database.
- The seed script has not been run.
- The Playwright end-to-end suite has not been executed — it skips itself unless E2E_USER_EMAIL / E2E_USER_PASSWORD are set.
- No screenshots of the running app exist.
Everything in “Getting started” is written to be followed exactly. If a migration fails on your project, that is a real bug — please treat it as one rather than assuming it was tested.
THREE SPACES, ONE SCHEMA, ONE IDENTITY
Vita is life, Opus is work, Studio is what you make. They are separate surfaces over a single Postgres schema and a single account — which is why a Today view can span them, and why the security model only has to be got right once.
Vita — life
The half of a personal system most tools refuse to hold, because it is not work and cannot be reported on.
WHAT IT HOLDS
- Personal tasks, separate from project tasks on purpose
- Habits and routines, with check-ins rather than streak theatre
- Meals and sleep logs
- Wellbeing check-ins — framed in-product as personal reflection, never as medical tooling
- A journal
- Goals
ROUTES
- /vita
- /vita/tasks
- /vita/habits
- /vita/routines
- /vita/meals
- /vita/wellbeing
- /vita/journal
- /vita/goals
TABLES — EACH WITH ITS OWN user_id
- personal_tasks
- habits
- habit_checkins
- routines
- routine_steps
- routine_checkins
- meal_logs
- wellbeing_checkins
- journal_entries
- goals
WHERE THE BOUNDARY ACTUALLY IS
Six layers sit between a request and a row. Three of them are convenience and three of them are the boundary, and being clear about which is which is most of the security model. Open each one.
Middleware
src/lib/supabase/session-proxy.ts
Redirects a signed-out visitor away from an app route before the page renders.
A routing gate. It decides what a browser is shown, which is a different question from what a database will hand over. SECURITY.md calls it “a convenience, not the boundary”, and that is the correct level of respect for it.
THE CONVENTION THE WHOLE SCHEMA RESTS ON
Flat policies are only possible because every child table carries its own user_id. The migration says so at the top, in a comment, before the first table exists:
-- * Every application table carries `user_id uuid not null references auth.users(id)
-- on delete cascade`, even child tables. This denormalisation is deliberate: it
-- lets every RLS policy be a flat `user_id = (select auth.uid())` instead of a
-- recursive EXISTS chain. Flat policies are faster, index-friendly and much
-- easier to audit. Triggers enforce that a child's user_id matches its parent's,
-- so the denormalisation cannot drift.
-- * `user_id` defaults to `auth.uid()`. Clients therefore cannot write rows for
-- another user even if they try to send one, and the WITH CHECK policy would
-- reject it anyway.THE TWO THINGS THAT GENUINELY RUN
Not everything in the project is un-portable. Two pieces are pure functions with no database underneath them, so they came across whole — and both of them are structural claims rather than features, which is why they were worth bringing.
One schema object, two call sites
The claim is not “it validates on the server as well”. It is that there is exactly one schema object, and both the form resolver and the Server Action import it — so they cannot drift apart the way a hand-written client check and a hand-written server check always eventually do. Type something wrong. Both panels below are calling the same object, so there is nothing you can do to make them disagree.
BROWSER — FORM RESOLVER
A convenience. It puts a message next to a field without a round trip.
REJECTED — 1
- dueDate · The due date cannot be before the start date
SERVER ACTION — BEFORE POSTGRES
Not a convenience. A request that never went near the form arrives here too.
REJECTED — 1
- dueDate · The due date cannot be before the start date
Identical, and not by agreement — by import. taskSchema is one object with one code path; the two panels are two call sites of it.
Recurrence, computed rather than stored
A routine stores a rule. Occurrences are derived on read. Nothing is written until a person actually records a check-in — which turns “this will not fill your database with a thousand speculative future rows” from a promise into a property of the design. This is src/lib/recurrence.ts, ported with no logic changed, running now.
MORNING TRIAGE
Every weekday (Mon–Fri)
Monday to Friday. Nothing on a Saturday, which is the point of having the frequency at all rather than seven checkboxes.
| M | T | W | T | F | S | S |
|---|---|---|---|---|---|---|
| 23 | 24 | 25 | 26 | 27 | 28 | 1 |
| 2 2026-03-02 due | 3 2026-03-03 due | 4 2026-03-04 due | 5 2026-03-05 due | 6 2026-03-06 due | 7 | 8 |
| 9 2026-03-09 due | 10 2026-03-10 due | 11 2026-03-11 due | 12 2026-03-12 due | 13 2026-03-13 due | 14 | 15 |
| 16 2026-03-16 due | 17 2026-03-17 due | 18 2026-03-18 due | 19 2026-03-19 due | 20 2026-03-20 due | 21 | 22 |
| 23 2026-03-23 due | 24 2026-03-24 due | 25 2026-03-25 due | 26 2026-03-26 due | 27 2026-03-27 due | 28 | 29 |
| 30 2026-03-30 due | 31 2026-03-31 due | 1 2026-04-01 due | 2 2026-04-02 due | 3 2026-04-03 due | 4 | 5 |
- OCCURRENCES IN VIEW
- 25
- NEXT AFTER
- 2026-04-06
- ROWS THIS WROTE
- 0