Skip to main content
Omnibeds keeps its data in Postgres, accessed with Drizzle. Three rules hold for every table: the schema comes from numbered migrations, every agency-owned query runs inside one agency’s scope, and writes that belong together run in one transaction.

Migrations

The schema is a list of numbered migrations in lib/db/migrations.ts. Each one is recorded in the schema_migrations table once it has run.
  • The pending migrations run once per server instance, in order, the first time any module needs the database (ensureSchema()).
  • npm run db:migrate runs them ahead of a deploy.
  • Every statement must be safe to run on a database that already has it, so two instances starting together are fine and a migration is recorded only once.

Add a migration

1

Append it

Add a new entry at the end of MIGRATIONS with the next id, a short name and its statements. Never edit a migration that has already run.
2

Make it idempotent

Use IF NOT EXISTS and ADD COLUMN IF NOT EXISTS, so running it twice changes nothing.
3

Test it

tests/migrations.test.ts builds a fresh database from every migration and checks that every table the code uses exists.

What is stored where

Bookings, quotes, trips and saved travellers are stored as a JSON document per row, next to the columns used to find them (agency, reference, dates).
The search results cache is the one thing kept in each server’s memory, on purpose: it lasts five minutes and losing it only means asking the supplier again.

Agency scope

Every request that touches agency data runs inside runForAgency. Every agency-owned query reads the agency id from that scope with currentAgencyId().
Outside a scope, currentAgencyId() throws. A forgotten scope fails closed instead of reading another agency’s data.
Commands run inside the caller’s agency automatically. A scheduled job loops over the agencies and runs once inside each one’s scope.

Transactions

withTransaction(fn) runs several writes as one. Every query made inside fn, from any module, joins the same transaction: it commits when fn returns and rolls back if it throws. Outside a database (unit tests with in-memory stores), fn simply runs. Linking a booking to a trip is a typical use: the trip’s list and the booking’s trip id are written together, so one is never saved without the other.
Never wrap a supplier call in a transaction. A booking made with a supplier can’t be rolled back, so the supplier call happens first and only our own writes go inside.
Rebooking and switching to a cheaper rate work this way: the new booking is made with the supplier, then the writes that link the old and new bookings are saved together. A trip lists its quotes and bookings, and each quote and booking also records its trip id. Linking and unlinking update both sides in one transaction, and deleting a trip clears the link on its records. To find a record’s trip, Omnibeds reads the record’s own trip id first.