> ## Documentation Index
> Fetch the complete documentation index at: https://demo-travel.webbinghub.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Data: migrations, agency scope and transactions

> How Omnibeds stores its data in Postgres: numbered migrations, every query scoped to one agency, several writes as one transaction, and two-way trip links.

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.

| Id | What it does |
| - | - |
| 1 | Baseline: every table as the modules created it |
| 2 | The trip of a quote or booking, indexed both ways |

* 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

| Kind of data | Tables |
| - | - |
| Agencies and their users | `agencies`, `users` |
| Bookings, quotes, trips, saved travellers | `bookings`, `quotes`, `trips`, `traveller_profiles` |
| Alerts in the bell | `deadline_alerts` |
| Stored hotel content | `hotels`, `hotel_supplier_links`, and one table per supplier's content |
| Supplier sessions and offer state | `supplier_state` |
| Call counts and quotas | `supplier_daily_calls`, `live_api_calls`, and the quota table |
| Shared runtime state | `circuit_breakers`, `rate_limits` |
| Exchange rates and place lookups | `fx_rates`, `place_lookups` |
| Logs | `traces`, `span_metrics` |

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).

> [!NOTE]
>
> 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()`.

> [!WARNING]
>
> 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.

> [!WARNING]
>
> 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.

## Trip links go both ways

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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.