Skip to content
newtheatrePublic

About

A simple room booking system for the Nottingham New Theatre.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Rooms: NNT Rehearsal Room Booking

Rehearsal room booking for the Nottingham New Theatre. Members request a room and a window; committee admins assign an internal room, arrange an external venue, or reject with a reason.

Live at: rooms.newtheatre.org.uk · Owner: IT Manager / Archivist · Auth: shared NNT sign-on via stage-door, this app never writes the session.

Stack

Layer Choice
Framework Nuxt 4, Vue 3, Nuxt UI 4
Server runtime Nitro on cloudflare_module, a Cloudflare Worker, not Node
Database SQLite: a local file in development, Cloudflare D1 in production
ORM Drizzle, via NuxtHub's hub:db layer
Auth nuxt-auth-utils, reading the estate's sealed nnt-session cookie
Email Resend
Content @nuxt/content v3 for the user guides under content/docs/

This app used Prisma until 2026-08-18; it now matches the rest of the estate (proscenium, stage-door, rehearsal). The existing production tables were kept as they were rather than rebuilt, which is worth understanding before you touch a migration: ADR-0001.

Quick start

This project uses Bun; bun.lock is the only lockfile.

git clone https://github.com/newtheatre/rooms && cd rooms
bun install
cp .env.example .env      # fill in per the comments in that file
bun run dev               # http://localhost:3000

There is no auth service running locally, so sign in with /dev-login (an ordinary member) or /dev-login?admin=1 (rooms:ADMIN). That route is guarded by import.meta.dev and does not exist in a production build.

How booking works

A request moves through one of three paths, all driven from /admin/bookings:

PENDING ──► CONFIRMED                      an internal room is assigned
        ──► AWAITING_EXTERNAL ──► CONFIRMED  an external venue is being arranged
        ──► REJECTED                        with a reason, shown to the requester

CANCELLED is terminal and available to the requester while a booking is PENDING or CONFIRMED.

A space is considered occupied by bookings in CONFIRMED, PENDING or AWAITING_EXTERNAL, a pending request holds its slot, so two people cannot both be told yes (server/utils/availability.ts). Admins can override a conflict deliberately.

Recurring requests expand to one booking row per occurrence at creation. There is no series entity: occurrences are approved, moved and cancelled individually.

Auth and roles

The estate's sealed nnt-session cookie is written only by auth.newtheatre.org.uk; this app reads it with getUserSession(). rooms:ADMIN is the only role this app owns; being signed in is enough to make a booking request.

The session contract is @newtheatre/auth-types, published from stage-door's packages/auth-types (stage-door ADR-0025). Do not redeclare it here: change the source and bump its version.

Full integration story: stage-door docs/integrating-an-app.md.

Commands

bun run dev        # local dev server
bun run lint       # eslint
bun run typecheck  # nuxt typecheck
bun run build      # the production Worker bundle
bun run db:generate # generate a migration from schema changes (review the SQL!)
bun run db:migrate  # apply migrations to the local database

CI (.github/workflows/ci.yml) runs lint, typecheck and build on every push and PR. Cloudflare Workers Builds deploys main; deployment is not CI's job.

Migrations are. .github/workflows/migrate.yml applies pending migrations on any push to main that touches server/db/migrations/**. Workers Builds only builds and deploys, and nothing applied migrations until this workflow existed; that gap took the estate down for an hour on 2026-08-19 (stage-door ADR-0021). Every run records a D1 Time Travel restore point before applying and gates on the _hub_migrations ledger afterwards. It needs a CLOUDFLARE_API_TOKEN repository secret with D1:Edit, and fails loudly without one.

./.github/scripts/pending-migrations.sh lists what is pending without applying it, and GET /api/health returns 503 naming the pending files if a deploy lands ahead of its migration.

Known gaps

Recorded here rather than left to be discovered:

  • There are no tests, and no test harness. proscenium is in the same position; rehearsal and stage-door are not.
  • Web push is not implemented. /api/notifications/subscribe records a subscription and the settings page offers the PUSH channel, but sendPushNotification logs and returns (server/utils/notifications.ts). Selecting PUSH delivers nothing.
  • Two tables still filter and page in the browser. /admin/bookings and /requests filter, sort and page client-side, so they walk every page of the API to get the whole list back (fetchAllPages). The endpoints themselves page in SQL; moving those two tables onto it is what remains, and until then they still transfer the whole table, just in 200-row requests.
  • PUT /api/bookings/bulk re-reads each booking after updating it, so a long batch is two round-trips per row. The writes themselves are one statement each, which the D1 parameter cap requires.
  • A stale session is only refreshed on navigation. app/middleware/admin.ts bounces through the auth service's refresh endpoint when the route middleware runs, but nothing intercepts a 401 { stale: true } from a $fetch. Those surface as an error toast, and the person has to navigate for the refresh to happen. Wiring an interceptor means deciding what it does to a half-filled form, which is why it has not been done here.
  • Venue assignment has no availability preview. There is no /api/venues/available to match the rooms one, so the admin venue picker does not mark which venues are taken. Assigning a clashing venue is still refused server-side with a 409, so this costs a wasted click rather than a double booking.

Note that content/docs/ is user-facing guidance published to the site. Engineering documentation lives in docs/.

Documentation

Doc Read it when…
docs/data-model.md you're changing the schema, or need the booking status lifecycle
docs/api-reference.md you're calling or changing an endpoint
docs/decisions/ you're about to ask "why on earth is it done this way?"
stage-door docs/integrating-an-app.md you're touching anything to do with sessions, roles or the GDPR hooks

Comments

A comment carries what the code cannot: a constraint, a trap, a contract that is not obvious from the signature. It does not narrate, and it does not restate the signature. Anything needing a paragraph of justification belongs in a decision record, not a header. See CLAUDE.md §Comments.

About

A simple room booking system for the Nottingham New Theatre.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages