Skip to content

Feat/postgres auth#1

Open
VanishJr wants to merge 7 commits into
mainfrom
feat/postgres-auth
Open

Feat/postgres auth#1
VanishJr wants to merge 7 commits into
mainfrom
feat/postgres-auth

Conversation

@VanishJr

Copy link
Copy Markdown
Owner

What this is

Roadmap week 1 — the foundation both remaining tracks depend on: E1 (SQLite → PostgreSQL) and B1/B2 (authentication and workspace isolation).

Done criterion from the roadmap is met: two users on one server cannot see each other's data, and the suite is green on Postgres.

Storage: PostgreSQL 16 + Drizzle (E1)

  • Schema now lives in backend/src/db/schema.ts. SQL migrations are generated from it (npm run db:generate) into backend/drizzle/ and applied at startup — the hand-rolled runner in src/migrations/ and its meta.schema_version counter are gone, as is better-sqlite3 as a runtime dependency (it stays in devDependencies for the import script below).
  • The old 001-initial / 002-money-cents migrations are folded into a single generated 0000_initial.sql. Nothing is deployed anywhere yet, so there is no history to preserve; from the first real deployment on, migrations become append-only.
  • The entire data layer is async now (db.ts was synchronous, the pg driver is not). That rippled through every caller: index.ts, classify, sync, fxrates, csv, settings, independence, all providers.
  • Backup/restore was built on db.backup() and ATTACH, both SQLite-only — it is a JSON export/import now, and the Overview screen button keeps working.
  • docker-compose.yml gains a postgres:16-alpine service with a healthcheck; 5432 is published so a non-Docker npm run dev talks to the same database. POSTGRES_PASSWORD is read from the repo-root .env by both the database and the backend's DATABASE_URL, so it changes in one place.

Auth: better-auth, email + password (B1)

  • Email+password with cookie sessions (scrypt, better-auth default). No client-side secret exists any more: the x-api-key shared secret and VITE_API_KEY are removed, CORS switched to an origin allowlist with credentials, and web/src/api.ts sends credentials: 'include'.
  • Every /api/* route requires a session. backend/src/index.ts is now a thin bootstrap; routes moved into backend/src/app.ts so tests can mount the app without starting a listener.
  • SIGNUP_INVITE_CODE gates registration. Empty by default (fine on a laptop, not fine on a public host); the register form asks for the code when the server requires it.
  • assertProductionConfig refuses to start under NODE_ENV=production on the dev auth secret or the dev database password — the failure mode this replaces was silent.
  • New web/src/screens/AuthScreen.tsx for login/register.

Workspaces: workspace_id everywhere (B2)

  • Every data table carries workspace_id; registration seeds a personal workspace via the better-auth organization plugin in a minimal configuration (roles owner/member, no permission matrix — companies are out of scope for the prototype).
  • The scope is not a parameter. It comes from AsyncLocalStorage (backend/src/db/workspace.ts): session middleware sets it per request, background jobs set it per workspace. A db call with no scope throws rather than quietly running unscoped — the failure is loud instead of being a data leak.
  • Bank credentials stay server-wide (MONOBANK_TOKEN, Enable Banking keys), so Monobank syncs only into the oldest workspace on the server and auto-sync serves that one alone. Otherwise other users would receive the owner's bank data. Documented in the README; per-workspace credentials are a later decision.

Tests

backend/test/setup.ts spins up PGlite (embedded Postgres, no Docker) with a fresh instance per test file, replacing DB_PATH=':memory:'. Existing tests are ported to await; restore-legacy.test.ts is dropped along with the SQLite backup format it covered.

New coverage:

  • workspace.test.ts — an unscoped db call throws.
  • isolation.test.ts — two workspaces over the real HTTP surface: neither sees the other's accounts, transactions, settings, rules or budgets.
  • http-auth.test.ts — register/login/logout, unauthenticated /api/* rejection, invite-code gating.
  • config.test.ts — production config assertions.

Migrating an existing database

npm run migrate:sqlite imports a pre-Postgres SQLite file into a single workspace (the oldest by default, --workspace <id> to pick another) and prints a per-table row count. One-off script, not part of the startup path.

Verification

  • backend: npm run typecheck clean, npm test — 13 files / 80 tests passing.
  • web: npm run build passing.
  • Manual: register two accounts on one server, import CSV into each, confirm neither sees the other's data.

Review notes

  • Commits are thematic rather than individually bisectable — db/index.ts, app.ts and index.ts each carry storage, auth and scoping changes at once, and splitting them further would have meant fictional intermediate states. Reviewing the diff as a whole is the honest path.
  • Largest single file to review is backend/src/db/index.ts; most of it is the mechanical raw-SQL → Drizzle port, with the workspace predicate added to every query.

@VanishJr
VanishJr requested a review from DmitriyRiffel July 21, 2026 07:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant