Skip to main content
Omnibeds has seven layers, and each one only calls the layer below it. Knowing which layer a change belongs to is the first decision you make on any piece of work.

Follow one request

This is what happens when an agent cancels a booking. Step through it to see which layer does what. The same path serves every action. Only the command and the service change.

Which layer does my change belong to

Screens only. If the screen needs data it can’t get yet, add or extend a command; never fetch a URL by hand.
A command in lib/app/commands/, plus a one-line route and its entry in the typed client’s ROUTES. See Commands.
The domain service. Commands stay thin and call it.
That supplier’s adapter in lib/suppliers/<supplier>/. Nothing above the adapter knows a supplier’s format. See Adapters.
A new migration. See Data.

Boundaries the tests enforce

These rules are checked on every test run by tests/boundaries.test.ts and tests/app-layer.test.ts, so breaking one fails the build:
  • Every command has a route, and that route file exists and serves the right HTTP method.
  • Every route that isn’t infrastructure is a one-line command route.
  • Code in lib/ never imports from components/.
  • A screen never imports server code. Only types cross into the browser.
  • A screen never writes a command’s URL by hand; it calls api().
A few routes sit outside the command layer on purpose because they aren’t actions on travel data: the session switcher, presenter unlock, the search stream and its sentence parser, the access gate, the guided tour, health, and the scheduled jobs.