DatabasesSupabase and Postgres
Two live apps, built on Supabase and Postgres.
Supabase is a hosted Postgres database with sign in, file storage and live updates built in. I run two things on it, each with its own database: the assistant on this site, and Velora, my expense tracker. In both, the rules that must never break, like who may see which data, are checked by the database itself, not by the app.
- Projects
- Two, separate, both live
- Database
- Postgres 17, with search by meaning
- Shared between them
- No data. One way of working
- Access
- Decided by the database
Each person sees only their own data
Velora keeps several people's expenses in one table. Below, the same request is sent three times by three different people, and three different answers come back. The database decides who gets what, so no screen or app can forget to check.

One request, three people
select id, merchant, amount
from expenses
order by created_at desc;In plain words: give me every expense, newest first.
Notice that no line says which person. That is not a trick. The database already knows who is asking.
Who is asking
- Streaming subscription€15.99
- Groceries€84.20
- Boiler repair€86.40
First account: 3 What comes back
What I built, in nine pieces
Across the two projects. Five belong to the assistant on this site, four to Velora.
Assistant
Search by meaning
Writing is stored as numbers, so a question finds the passage that means the same thing even when the words are different.
Assistant
The database does the search
The database ranks the matches and hands back the few that count. The app asks a question instead of doing the maths.
Assistant
A second, looser search
A strict search runs first. When nothing is close enough, one looser search runs rather than answering with nothing.
Assistant
Facts in plain tables
Six small tables hold the facts about me, so my job title comes from a row and never from a model's memory of one.
Velora
Each person sees only their own rows
The database decides which rows a signed in person may read, on every request, so no app has to remember to check.
Velora
One step fixes every past expense
Change a recurring price and one routine goes back and corrects every expense that rule already covers.
Velora
Delete now, remove for good later
Deleting hides a row for thirty days. A scheduled job then removes what is older, behind a secret only that job knows.
Velora
Live on every device
Each edit reaches every open session as it happens, so two devices looking at the same month never disagree.
Assistant
Facts come from one reviewed file
Those fact tables are rebuilt from a file kept with the code, so what is live cannot quietly drift from what was reviewed.
How both databases are run
Four rules I follow in both projects.
- 01
Let the database decide who sees what
In Velora, access rules live in the database and Postgres checks them on every request. The assistant has no user accounts, so its tables are closed to the outside and read only from the server.
Prevents: No leak from a forgotten check
- 02
One job per table
The search index and the facts about me are separate tables, because they answer different questions and fail in different ways.
Prevents: No table doing two jobs
- 03
The database does the hard parts
Anything that has to be exactly right is a routine inside Postgres. The app asks for an answer instead of assembling one.
Prevents: No correctness that depends on which app is asking
- 04
Facts come from one reviewed file
The fact tables are rebuilt from a file kept with the code, so there is one place to change a fact and one place to review it.
Prevents: No fact that exists only in the live database
My role
Both projects are mine end to end, so the layout of both databases, the routines, the access rules and the releases are all decisions I made and had to live with.
- 01Designed the layout of both databases: what each table is for, what it holds, and what belongs somewhere else.
- 02Wrote the Postgres routines, including the search behind this site's assistant and the correction behind Velora's recurring rules.
- 03Wrote the access rules and chose what happens when a check is missing: an empty result rather than an error, so a gap is silent for an attacker and loud for the tests.
- 04Split the assistant's knowledge in two, a search index for prose and plain tables for facts, after the model kept improvising a job title.
- 05Handled the boring half: separate keys for separate privileges, a scheduled clean up behind a secret, and a cap on how often the paid step can be called.
The first thing I would change is how the databases are updated. Both grew by hand, which works while one person holds the whole picture and stops working the moment that is not true.
What changed
2
Separate databases, run the same way
A screen added tomorrow cannot forget the access check. A second app cannot skip the fix. A question about my job title is answered from a row, so the worst case is that the row is out of date rather than that the answer was made up.
- Before
- After
- Every screen had to remember to check who was asking
- The database checks on every request
- A price change fixed one row at a time
- One step corrects every expense the rule covers
- Every record downloaded to the app before anything was compared
- The database returns the few that match
- Facts edited by hand, drifting quietly
- Facts rebuilt from a file anyone can review
0
Access checks living outside the database
1
Step to fix every past expense
6
Fact lists checked before the assistant answers
Built with
- Postgres 17
- pgvector
- Supabase
- TypeScript
- Next.js
- The Supabase MCP server