ai newsMi., 4. Nov.
EilmeldungOpenAI acquires Pierre Computer Company to make Git keep up with AI coding agents

How to build a room booking app with an AI agent using the Prisma stack

Twenty-five requests for the same room at the same moment: one booking, twenty-four refusals. This post builds the app that does that, from an empty folder to a live URL, with an AI coding agent doing the typing and the Prisma stack doing the rest: Prisma ORM for the data, Prisma Postgres for the d

dev.toMittwoch 19:01 · 04.11.14 Min. Lesezeit
Bild: dev.to

Posted on Nov 4 • Edited on Nov 4 • Originally published at mohammedobaid.in

Twenty-five requests for the same room at the same moment: one booking, twenty-four refusals. This post builds the app that does that, from an empty folder to a live URL, with an AI coding agent doing the typing and the Prisma stack doing the rest: Prisma ORM for the data, Prisma Postgres for the database and Prisma Compute for hosting. Prisma's skills tell the agent how the platform works today, one Composer module provisions the database, and one workflow deploys every push.

This build was done with Claude Code and Opus 5.5. Any similar agent or model that can run shell commands and read Agent Skills can follow the same path. Everything here comes from building a template for Prisma's app gallery in October 2026: the prompt, the code that matters, and what each Prisma piece took off your plate.

Rooms are rows on a timeline. Drag across free time to book, drag a booking to move it, drag its edge to resize it, click it to cancel. A taken slot turns red under the cursor before you drop, and the server enforces the same rules when two people race for the same slot. An empty database seeds six rooms and a day of demo bookings on the first visit, so a fresh deploy has something to show.

The interface is a glassmorphism theme, in light and dark: a clean, layered interface that suits a modern productivity tool. The page is a frosted window floating over soft fields of colour, and translucent layers blur what sits behind them ( blur(28px) saturate(170%) ), each at its own depth. The room panel, the timeline and the "Built with" cards are modular glass cards, controls are raised off them, and dialogs, popovers and toasts float above everything on their own overlay glass. A single violet accent carries the focus. The theme follows the system setting, and the theme button next to Export and Print switches it by hand.

Every value is a design token: a CSS custom property defined once with light-dark() ( --glass, --glass-card, --glass-raised, --glass-float, --blur, --accent and so on). The scheduler is themed through the same tokens, with its --rs-* variables mapped onto them, so a new look is a change to one block and reaches every component. Bookings can also be exported as a calendar file or printed.

Prisma ships instructions for agents as skills: a SKILL.md file plus reference material, which, per Prisma's docs, the agent uses on its own when it detects a relevant task. That keeps the agent on the platform as it is today, not as its training data remembers it. Install the one for Compute:

npx skills add prisma/skills --skill prisma-compute Enter fullscreen mode Exit fullscreen mode The skills for Prisma ORM 8 ( prisma-8 ) and for Composer ( prisma-composer-core-concepts ) ship inside the packages themselves, so they match the version you install. prisma skills sync copies them into the folders agents read (.claude/skills,.cursor/skills,.agents/skills,.devin/skills ), and prisma init runs it and adds a postinstall hook that keeps them in sync.

What the skill changed on this build: prisma-compute (v2.0.0) says that the old prisma.compute.ts, defineComputeConfig and compute:deploy flow is gone and its commands now fail with CLI.UNKNOWN_COMMAND. The agent followed the skill and went straight to what exists today: a Composer module in module.ts, a service in service.ts, and prisma deploy module.ts. The same research, from the skills and the docs source, found three more things that shaped the build:

Install only the skills you need. The full registry also includes older Prisma 7 skills, and Prisma's README says not to use their legacy recipes for ORM 8 setup.

Then give the agent the prompt. This one reproduces the app in this post:

Build a meeting room booking app as a template for Prisma's app gallery. Stack - Next.js 16 (App Router), TypeScript, npm. - Resource Scheduler (the resource-scheduler npm package) for the timeline. - Prisma ORM 8, Prisma Postgres and Prisma Compute, deployed as a Prisma Composer module (module.ts and service.ts). Follow the Prisma skills for every platform decision.

Product - Six rooms on two floors: Atlas, Borealis and Cirrus on Floor 1, Delta, Echo and Fjord on Floor 2, with 2 to 20 seats. - A day and week timeline, rooms as rows grouped by floor. Drag across free time to book, drag a booking to move it, drag its edge to resize it, click a booking to cancel it. A slot that cannot be booked turns red before the drop. - A "Find a time" panel: pick a length and a group size, see which rooms are free. - A utilization row, calendar export (.ics) and print. - Opening hours 08:00 to 18:00 and 30-minute slots on the office clock (Europe/Berlin).

Nothing can be booked in the past. - An empty database seeds the rooms and a day of demo bookings on the first visit. - A glassmorphism look in light and dark mode: a frosted, translucent window over soft fields of colour, with one violet accent. Follow the system setting and add a theme button. Every colour, blur and radius is a design token (CSS custom properties).

Rules - Two people booking the same room at the same moment must never both succeed. Lock the room row inside a transaction before checking, in a fixed order, and add a database exclusion constraint on (room, time range) as a backstop. - One rule set for the browser and the server, independent of the machine's time zone. - Optimistic updates that roll back with the server's message if someone else wins. Delivery - Tests that race real concurrent requests against real PostgreSQL. - Run it locally the way Compute runs it. - Deploy every push with a GitHub Actions workflow: the default branch to production, other branches to previews, signed in with OIDC, no secrets. - Add a Deploy Button to the README and keep the repository within its limits: public, Composer module at the root, a build script, one lockfile, at most 100 files, no file over 5 MB, no symlinks. - Tell me when you need a login that only I can do.

Enter fullscreen mode Exit fullscreen mode Step 2: Scaffold the project The agent scaffolded with create-prisma, which needs no prompts:

npx create-prisma@0.14.6 room-booking --template next \ --provider postgres --authoring psl --package-manager npm --skills none \ --yes --json --no-deploy Enter fullscreen mode Exit fullscreen mode It prints one JSON line ( {"ok":true,...,"nextSteps":} ) listing what to run next, so an agent has no prose to parse. Drop --skills none if you want the scaffold to set up the skills for you; per Prisma's docs it runs prisma init.

You get Next.js 16, a Composer module ( module.ts ), a service ( service.ts ), a prisma.config.ts with an orm and a composer section, a starter contract, and a starter migration already planned for it (Step 3 replaces it). The finished template pins the toolchain to what the scaffold installs. At the time of writing that is Composer 0.35.0, prisma 8.0.0-rc.28, @prisma/orm-postgres 8.0.0-rc.20 and alchemy 2.0.0-beta.83, with the scaffold's effect 4.0.3 overrides. Prisma ORM 8 is moving fast, so check what the scaffold installs when you read this.

One thing to add to the scaffold's dependencies: the deploy loads @effect/platform-bun through alchemy, and npm did not install it at the top level of the project, so the deploy failed with "Alchemy could not load the required peer dependency". The template lists @effect/platform-bun, @effect/platform-node and @effect/platform-node-shared (all 4.0.3) as dev dependencies.

A room has bookings, and a booking has a time range. In Prisma ORM 8 that is a contract in src/prisma/contract.prisma:

The scaffold already shipped a starter migration for its own contract, so remove it before planning yours, or migration plan fails with MIGRATION.PLAN_ORIGIN_UNKNOWN ( --from @empty works too). Then generate the types and plan the migration:

rm -r migrations/app/ * migrations/snapshots/ * npm run contract:emit npm run migration:plan -- --name init Enter fullscreen mode Exit fullscreen mode migration plan prints the full SQL it is about to put in the migration, so the agent (and you) can review it before anything runs.

The contract cannot express a constraint that forbids overlapping time ranges, but PostgreSQL can. Prisma's docs say to put what the contract cannot express in a raw SQL step, so the agent edited the planned migration.ts: createExtension('btree_gist') first, then a rawSql step at the end:

ALTER TABLE "public". "Booking" ADD CONSTRAINT "booking_no_overlap" EXCLUDE USING gist ( "roomId" WITH =, tstzrange ( "startsAt", "endsAt", '[)' ) WITH && ) Enter fullscreen mode Exit fullscreen mode Then recompile the migration with node migrations/app/<migration>/migration.ts. Never edit the generated ops.json by hand. prisma db verify (without --strict ) accepts the extra constraint instead of flagging it as drift. btree_gist is on Prisma Postgres's list of supported extensions and also exists in the local emulator, so the same migration runs locally, in previews and in production.

In Prisma ORM 8 the double-booking guard is a row lock: writers for the same room queue. (Transactions in ORM 8 do not offer isolation levels yet, so a Serializable transaction is not an option.) Each write locks the room row, reads the overlapping bookings, and only then checks and writes:

// src/server/bookings.ts, shortened return db. transaction ( async ( tx ) => { // Lock the room rows in id order: writers for the same room queue here. for ( const id of [... new Set ( roomIds )]. sort ()) { await tx. orm. public. Room. where ({ id }). forNoKeyUpdate (). first (); } // Read the overlapping bookings, check them with the same rules the // browser uses (isPlacementAllowed from resource-scheduler/headless), // and write only if the placement is allowed. }); Enter fullscreen mode Exit fullscreen mode Same lock order everywhere. A move locks the booking first, then both rooms sorted by id, so concurrent writers queue instead of deadlocking.

FOR NO KEY UPDATE rather than FOR UPDATE, so other transactions' foreign-key checks on rooms are not blocked. The database as a backstop. If a code path ever skips the lock, the exclusion constraint from Step 3 makes PostgreSQL refuse the overlap ( 23P01 ), and the API turns that into the same 409.

One rule set for browser and server. The rules live in src/lib/rules.ts and run in both places. They must not depend on the machine's time zone: getHours() gives different answers in a browser in Berlin and a server in UTC, so the rules convert with utcToZoned(start, OFFICE_TIME_ZONE) on both sides.

Precise errors. 400 with per-field messages (zod), 404, 409 naming the booking in the way ( Atlas is already booked from 9:00 AM to 9:30 AM ("Daily standup"). ), and 422 for rule breaks such as opening hours. The evidence:

There are no retries and no serialization failures to handle, and the losing request gets a precise message instead of "try again".

The board is Resource Scheduler: rooms are resources grouped by floor, bookings are events. The agent wired it from the package's types and docs:

One import matters: the server must import isPlacementAllowed from resource-scheduler/headless. The main entry starts with "use client", while the headless entry imports only date-fns and is safe on the server.

Bookings are optimistic: a grey pending card appears at once. If two people race for a slot, the loser's card is rolled back with the server's message ( Delta is already booked from 1:00 PM to 1:30 PM ("Winner"). ) and the board reloads the visible day, so the loser can see what is in the way. The agent checked all of this in Chromium with Playwright: drag-select, empty submit, red footprint with no request, two tabs racing, move, cancel and next day, with no console errors.

npm run dev:composer Enter fullscreen mode Exit fullscreen mode This builds the app and brings up module.ts with prisma dev: the production build running on Bun, against a local Prisma Postgres emulator, with no credentials and no account. Composer applies the committed migrations, including the hand-written constraint, to an empty database. It was up in about 11 seconds. The first page load seeded the demo data in 0.62 s, and the next took 0.02 s. prisma dev runs in the foreground (Ctrl+C stops it).

What you run locally is what Compute runs: the same production build on the same Bun runtime.

For tests, npm test runs 38 unit tests anywhere. With TEST_DATABASE_URL pointing at a throwaway PostgreSQL database it also runs 17 integration tests, after applying the migrations the same way a deploy does. Use real PostgreSQL for those, not the emulator: the emulator is PGlite, which accepts one connection at a time and queues the others, so row locks between "two users" mean nothing there.

Keep local settings in.env.local, which next build never copies into the deployed bundle.

Two small files describe the whole deployment. module.ts declares a Prisma Postgres database built from the contract, and the app that depends on it:

// module.ts import { module } from " @prisma/composer "; import { postgres } from " @prisma/composer-prisma-cloud/orm "; import { appContract } from "./src/prisma/composer.ts "; import app from "./service.ts "; export default module ( " room-booking-prisma-template ", ({ provision }) => { const database = provision ( postgres ({ name: " database ", contract: appContract, config: "./prisma.config.ts " }), { id: " database " }, ); provision ( app, { deps: { database } }); }); Enter fullscreen mode Exit fullscreen mode service.ts declares the Next.js service and its dependency on that database:

// service.ts import nextjs from " @prisma/composer/nextjs "; import { compute } from " @prisma/composer-prisma-cloud "; import { postgres } from " @prisma/composer-prisma-cloud/orm "; import { appContract } from "./src/prisma/composer.ts "; export default compute ({ name: " app ", deps: { database: postgres ( appContract ) }, build: nextjs ({ module: import. meta. url, appDir: ". " }), }); Enter fullscreen mode Exit fullscreen mode The app gets its database client from Composer ( service.load() ), not from a DATABASE_URL, so there is no database URL to set in production. Keep output: "standalone" in next.config (the scaffold already sets it) and nextjsBuild() in the composer extensions of prisma.config.ts: without the standalone output, Prisma's docs warn, the deploy "completes" and the URL returns 504.

Deploy on every push with a GitHub Actions workflow. It signs in with GitHub's OIDC token, so the repository holds no secrets:

#.github/workflows/prisma-deploy.yml, trimmed name: prisma-deploy on: push: permissions: contents: read id-token: write jobs: deploy: if: github.ref_type == 'branch' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version-file:.nvmrc - uses: oven-sh/setup-bun@v2 - uses: prisma/cloud-deploy-action@v1 with: build-command: npm run build Enter fullscreen mode Exit fullscreen mode Connect the repository to a Prisma project once. From the CLI that is three commands: deploy once ( npm run deploy ), then npx prisma project link <name>, then npx prisma git connect. Or import the repository in the Console, which also opens the workflow pull request.

After that, a push to the default branch deploys production, and a push to any other branch deploys a preview named after it, each with its own database. The first production deploy took 76 seconds in GitHub Actions, 34.5 s of it prisma deploy. The page then read 6 rooms and 12 bookings from Prisma Postgres, with a 13 ms query.

Compute runs the app on Bun in one region, fixed at the first deploy.

To let other people copy the project, add a Deploy Button to the README. It needs a public repository with the Composer module at its root, a build script, one lockfile, at most 100 files and 25 MB, no file over 5 MB, and no symlinks or submodules. The button links to console.prisma.io/new/clone?repository-url=<your repo>&project-name=<name>. Prisma copies the repository into the visitor's GitHub account, creates a project and adds its own deploy workflow. Because Composer provisions the database, the first deploy needs no secrets, which is what a button URL requires. Before you share the button, click it from a clean account and check that the app comes up.

Next.js needs very little to run on the Prisma stack:

The README has a "Deploy with Prisma" button, and the link behind it is all a reader needs:

There are no environment variables or secrets to enter. A copy made this way went from click to live app in about two and a half minutes, and the Console listed the project as live.

The finished repository has no database URL in production, no migration job in CI and no deploy secrets. Each piece of the stack took one of those away:

With Prisma, the infrastructure of an app like this shrinks to three small files: a contract, a module and a workflow. That is a shape an agent handles well:

For a new app on Prisma ORM 8 (at the time of writing, a release candidate), the whole path works end to end: one deploy provisions the database, applies the migrations and starts the app.

Quelle: dev.to (dev.to). Original öffnen