A database, user login, and per-user data isolation — three hard things, solved at once. And the two quiet ways it bites: prices come back as NaN, or every user can read everyone's rows.
The row was right there in Supabase — a clean price, a real number. The app read it and rendered NaN. No error. No crash. The pipeline just produced garbage out of perfectly good data.
"The number was in the database. I could see it. The app turned it into NaN. I kept staring at the math thinking the calculation was wrong — the calculation was fine."
"The database speaks snake_case — base_price. The code asked for basePrice. That field doesn't exist, so it comes back undefined, and undefined in math is NaN. I should've flagged the naming mismatch immediately instead of letting you chase the arithmetic."
To have real users with real saved data, you normally need three separate, genuinely hard systems: a database, user login, and a way to make sure each person only sees their own rows. Supabase hands you all three as a managed service you call from your app — no server to run.
a real databaseA full SQL database (Postgres) behind a friendly client. my-day runs a 17-table schema on it.
login, handledSign-up, sign-in, sessions, password resets — the account layer built for you, not from scratch.
row-level securityRules in the database that decide which user can see which rows — per-user isolation, enforced at the data.
The two classic bites, both silent: forget to turn on RLS and every user can read everyone else's rows (a privacy hole with no error). And the database's snake_case column names won't match your code's camelCase — so fields come back empty, and empty numbers become NaN. Neither throws. Both just quietly produce the wrong thing.
The row clearly has base_price = 189. Your code reads it and gets NaN, no error. What broke?
snake_case; JS code tends to write camelCase. Ask for basePrice on a row that only has base_price and you get undefined — and undefined * 1.1 is NaN. The fix is matching the names (or mapping them), not the math. (The nastier cousin: forgotten RLS, where the data loads fine but everyone's rows load.)snake_case — your code has to match them exactly.user_id = auth.uid().Same rule as always: answer it right, or the module isn't done.
auth.uid(), then verify it against the live tables, not the migration files. Module unlocked. ↓Supabase gives you DB + auth + isolation for free — but the isolation (RLS) is opt-in, and silence is not safety. Match your field names, and never trust a table you haven't checked has a policy.
A real supabase-client.js starter — reads its URL and key from env vars (never hard-coded), and includes the snake_case ↔ camelCase trap written into a comment so you don't get bitten by the NaN bug. Paired with an RLS audit checklist that makes you enumerate the live tables (the exact mistake of auditing policies-on-paper instead of real tables).