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.

READ ABOUT THE SYSTEM

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

Checks that pass locally on the DENAVUM repository
CHECKRESULT
npm run typecheckpasses, zero errors (TypeScript strict + noUncheckedIndexedAccess)
npm run lintpasses, zero errors (6 informational React Compiler warnings)
npm run test91 tests pass across 5 files
npm run buildproduction 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.

Six weeks from 2026-02-23. Dates the rule is due are marked “due”.
MTWTFSS
2324252627281
2 2026-03-02 due3 2026-03-03 due4 2026-03-04 due5 2026-03-05 due6 2026-03-06 due78
9 2026-03-09 due10 2026-03-10 due11 2026-03-11 due12 2026-03-12 due13 2026-03-13 due1415
16 2026-03-16 due17 2026-03-17 due18 2026-03-18 due19 2026-03-19 due20 2026-03-20 due2122
23 2026-03-23 due24 2026-03-24 due25 2026-03-25 due26 2026-03-26 due27 2026-03-27 due2829
30 2026-03-30 due31 2026-03-31 due1 2026-04-01 due2 2026-04-02 due3 2026-04-03 due45
OCCURRENCES IN VIEW
25
NEXT AFTER
2026-04-06
ROWS THIS WROTE
0