Weekly schedule, patient roster, and session notes for a psychology practice. Local-first, PT/EN.
See the codeA web app for a psychologist's practice: book sessions, keep a patient roster, and write session notes.
Live: https://www.datapsi.com.br · Portuguese and English, toggled in the top-right corner.
It is local-first: everything works on one device with no account and no network. Two optional layers protect the data:
Session notes are clinical data, so the design starts from "the server should never be able to read them".
localStorage.auth.uid() = user_id) means a user can only read or write their own rows, even with the public anon key.Leave the Supabase variables unset and the app runs exactly as before, on one device only.
The home page is a contents list leading to each one.
The schedule grid.
A roster of names with an optional reference note. The roster feeds the Agenda's suggestions and the Evolution patient selector.
Session notes per patient, filed by date, newest first.
supabase/schema.sql.main on every push. Sync is enabled there by setting the two VITE_SUPABASE_* variables in the project's environment; without them the same build runs local-only.src/
lib/ schedule, patients, evolution, history, storage
vault.ts on-device encryption behind a passcode
account.ts master key, password and recovery-key wrapping
sync.ts encrypt → push / pull → decrypt
auth.ts, supabase.ts
components/ grid, calendar, editors, lock screen, account gate
pages/ Home, Agenda, Patients, Evolution, History
supabase/ schema.sql (tables and RLS policies)
npm install
npm run dev
Open http://localhost:5173. It runs local-only out of the box.
supabase/schema.sql in its SQL editor..env.example to .env.local, then fill in VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY.
service_role key here.Two suites, weighted towards the places where records could go missing quietly, not towards coverage numbers.
Unit tests: Vitest, 93 tests over src/lib:
To check the suite actually catches things, three real past bugs were put back in one at a time: an hour-shift in v1 data, a swallowed database error, and a repeating recovery key. Each one turns it red. Writing the suite also exposed a real lost-write bug: lock() didn't wait for the encrypted write to finish. That is now fixed.
End-to-end tests: Playwright, 30 tests over the local-only flows:
The app is started with Supabase disabled, so no test touches a real backend.
CI: every pull request and every push to main runs a type check, the unit tests, a production build, and the Playwright suite.
npm test # unit tests
npm run coverage # unit tests with coverage
npm run e2e # Playwright (first run: npx playwright install chromium)
npm run build
The output in dist/ is static and can be hosted anywhere.
TypeScript
83.1%
CSS
13.6%
PLpgSQL
2.1%
HTML
1.2%
Weekly schedule, patient roster, and session notes for a psychology practice. Local-first, PT/EN.
See the codeA web app for a psychologist's practice: book sessions, keep a patient roster, and write session notes.
Live: https://www.datapsi.com.br · Portuguese and English, toggled in the top-right corner.
It is local-first: everything works on one device with no account and no network. Two optional layers protect the data:
Session notes are clinical data, so the design starts from "the server should never be able to read them".
localStorage.auth.uid() = user_id) means a user can only read or write their own rows, even with the public anon key.Leave the Supabase variables unset and the app runs exactly as before, on one device only.
The home page is a contents list leading to each one.
The schedule grid.
A roster of names with an optional reference note. The roster feeds the Agenda's suggestions and the Evolution patient selector.
Session notes per patient, filed by date, newest first.
supabase/schema.sql.main on every push. Sync is enabled there by setting the two VITE_SUPABASE_* variables in the project's environment; without them the same build runs local-only.src/
lib/ schedule, patients, evolution, history, storage
vault.ts on-device encryption behind a passcode
account.ts master key, password and recovery-key wrapping
sync.ts encrypt → push / pull → decrypt
auth.ts, supabase.ts
components/ grid, calendar, editors, lock screen, account gate
pages/ Home, Agenda, Patients, Evolution, History
supabase/ schema.sql (tables and RLS policies)
npm install
npm run dev
Open http://localhost:5173. It runs local-only out of the box.
supabase/schema.sql in its SQL editor..env.example to .env.local, then fill in VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY.
service_role key here.Two suites, weighted towards the places where records could go missing quietly, not towards coverage numbers.
Unit tests: Vitest, 93 tests over src/lib:
To check the suite actually catches things, three real past bugs were put back in one at a time: an hour-shift in v1 data, a swallowed database error, and a repeating recovery key. Each one turns it red. Writing the suite also exposed a real lost-write bug: lock() didn't wait for the encrypted write to finish. That is now fixed.
End-to-end tests: Playwright, 30 tests over the local-only flows:
The app is started with Supabase disabled, so no test touches a real backend.
CI: every pull request and every push to main runs a type check, the unit tests, a production build, and the Playwright suite.
npm test # unit tests
npm run coverage # unit tests with coverage
npm run e2e # Playwright (first run: npx playwright install chromium)
npm run build
The output in dist/ is static and can be hosted anywhere.
TypeScript
83.1%
CSS
13.6%
PLpgSQL
2.1%
HTML
1.2%