# Getting started ## First: give your agent the skill Composer is built to be driven by an agent, and the skill that teaches it the API ships inside `@prisma/composer` itself. So the setup is two lines in [step 1](#1-project-setup) below — install the package, then copy the skill out of it: ```sh pnpm prisma skills sync ``` Your agent then knows the entire API and arrives prepped with the building blocks it can compose — the ready-made Modules for scheduled jobs, blob storage, and event streams, plus the ones you write. From there you describe what you want ("a Next.js storefront calling an orders API with its own Postgres, deployed to a staging stage") and it composes the app; you review TypeScript, not YAML. Do this even if you intend to write every line yourself. It costs one command, and it stops your agent inventing an API that doesn't exist the first time you ask it for help. Because the skill travelled inside the package, what your agent reads is the API of the version you installed — not whatever was on `main` the day it was fetched. It works because of three properties you'll see throughout this guide: capabilities arrive as **Modules** that snap together instead of integrations you assemble; **the compiler checks the wiring**, so a mistake fails `tsc` in seconds rather than a deploy ten minutes later; and **the deploy is deterministic** — one command, no infrastructure config, and re-running it converges instead of drifting. ## The rest of this guide The point of what follows is that you can read what your agent writes. It takes you from an empty directory to a two-service app running on Prisma Cloud, meeting every core idea once: a contract, a service, a root module, a build, a deploy. At the end there's a section on [porting an app you already have](#porting-an-existing-app). The app is deliberately tiny — a `quotes` API and a public `gateway` that calls it, no database — so you can see the whole shape at once. Adding a Postgres (including a Prisma-ORM-typed one) is the first thing to do after, and [Building an app](building-an-app.md#databases) covers it. You'll need: - **Node 22.18 or newer** — check with `node --version` before anything else. Composer hands your TypeScript entry file straight to Node, and Node runs `.ts` directly only from 22.18.0, the release that turns type stripping on by default. On anything older `prisma-composer` stops at `ERR_UNKNOWN_FILE_EXTENSION` naming your own file; 22.17 is not close enough. - [Bun](https://bun.sh) — Prisma Compute runs Bun, so that's what the server code targets (`Bun.serve`), and it's the fastest way to run things locally. - pnpm (or npm). - For the deploy at the end: a Prisma Cloud workspace, plus a service token and your workspace id from the [Prisma Console](https://console.prisma.io). (Naming, once: **Prisma Cloud** is the platform; **Prisma Compute** runs your services on it, and **Prisma Postgres** hosts the databases.) ## 1. Project setup ```sh mkdir my-app && cd my-app && pnpm init pnpm add @prisma/composer @prisma/composer-prisma-cloud arktype pnpm add -D typescript @types/bun prisma pnpm prisma skills sync ``` `prisma` is the Prisma CLI; `prisma skills sync` copies the skill out of the installed `@prisma/composer` into the skill directories your agent runtimes read (`.claude/skills/`, `.cursor/skills/`, `.agents/skills/`, `.windsurf/skills/`). Add it to `postinstall` so an upgrade brings the matching skill with it — `|| exit 0` keeps installs that have no `prisma` binary (a production install with no dev dependencies) from failing: ```jsonc // package.json "scripts": { "postinstall": "prisma skills sync || exit 0" } ``` Those copies are derived from your lockfile, like `node_modules` — gitignore them rather than committing them. ```jsonc // tsconfig.json { "compilerOptions": { "target": "ES2022", "module": "Preserve", "moduleResolution": "bundler", "noEmit": true, "strict": true, "skipLibCheck": true, "types": ["bun"] }, "include": ["module.ts", "src"] } ``` Within your entry graph you may write relative imports as `./service.js` or extensionless `./service` — both forms resolve correctly under both runtimes. The CLI maps `.js`/extensionless specifiers to the matching `.ts` source under Node; Bun does this natively. This is what you're about to create: ``` my-app/ ├── module.ts # the root module — the app itself ├── prisma.config.ts # config: the `composer` section (read only by the CLI) └── src/ ├── quotes/ │ ├── contract.ts # the quotes service's public API, as types │ ├── service.ts # what quotes is: deps + build + what it exposes │ └── server.ts # the code that actually runs └── gateway/ ├── service.ts └── server.ts ``` ## 2. The quotes service Three files. First, the **contract** — the API other services will call, written as schemas. It lives with the service that owns it. Any [Standard Schema](https://standardschema.dev) validator works (arktype, zod, valibot…); the examples use arktype: ```ts // src/quotes/contract.ts import { contract, rpc } from '@prisma/composer/service-rpc'; import { type } from 'arktype'; export const quotesContract = contract({ random: rpc({ input: type({}), output: type({ quote: 'string' }) }), }); ``` Second, the **service declaration**. This is pure data — no behavior. It says what the service is called, what it depends on (nothing yet), how it's built, and which contract it exposes: ```ts // src/quotes/service.ts import node from '@prisma/composer/node'; import { compute } from '@prisma/composer-prisma-cloud'; import { quotesContract } from './contract.ts'; export default compute({ name: 'quotes', deps: {}, build: node({ module: import.meta.url, entry: '../../dist/quotes/server.mjs' }), expose: { rpc: quotesContract }, }); ``` Third, the **server** — the code your build turns into `dist/quotes/server.mjs` and the platform boots. `serve()` generates the HTTP handler from the contract; if you forget a handler or return the wrong shape, it doesn't compile: ```ts // src/quotes/server.ts import { serve } from '@prisma/composer/service-rpc'; import service from './service.ts'; const port = service.port(); // the reserved port, resolved (default 3000) const QUOTES = [ 'Simplicity is prerequisite for reliability.', 'Make it work, make it right, make it fast.', ]; const handler = serve(service, { rpc: { random: async () => ({ quote: QUOTES[Math.floor(Math.random() * QUOTES.length)]! }), }, }); export default handler; // Bind all interfaces — Compute routes external HTTP to the VM, so a // loopback-only listener would be unreachable. Bun.serve({ port, hostname: '0.0.0.0', fetch: handler }); ``` Notice what's missing: no URL of anything, no `process.env`. Every service gets a port for free (default 3000), read through `service.port()`; dependencies arrive through `service.load()`, and any configuration of your own through `service.input()` ([Building an app](building-an-app.md#service-input)) — three typed accessors, and that's the whole framework contract with your code. ## 3. The gateway service The gateway depends on the quotes contract. Notice the asymmetry with §2: the quotes service *exposed* the bare contract (its offer); the gateway wraps it in `rpc()` (its need — "a client of this contract"). Declaring `deps: { quotes: rpc(quotesContract) }` means `service.load()` hands the server a ready-made, typed client — calling it is just an async function call: ```ts // src/gateway/service.ts import node from '@prisma/composer/node'; import { rpc } from '@prisma/composer/service-rpc'; import { compute } from '@prisma/composer-prisma-cloud'; import { quotesContract } from '../quotes/contract.ts'; export default compute({ name: 'gateway', deps: { quotes: rpc(quotesContract) }, build: node({ module: import.meta.url, entry: '../../dist/gateway/server.mjs' }), }); ``` ```ts // src/gateway/server.ts import service from './service.ts'; const { quotes } = service.load(); const port = service.port(); Bun.serve({ port, hostname: '0.0.0.0', fetch: async () => { const { quote } = await quotes.random({}); return new Response(quote); }, }); ``` The gateway exposes no contract of its own, so there's no `serve()` here — it's an ordinary HTTP server that happens to receive a typed client. ## 4. Compose the app The root module is the app. It provisions both services and wires the quotes service's exposed port into the gateway's dependency slot — `provision()` returns a ref carrying one port per exposed contract, so `quotes.rpc` exists because the service declared `expose: { rpc: … }`: ```ts // module.ts import { module } from '@prisma/composer'; import gatewayService from './src/gateway/service.ts'; import quotesService from './src/quotes/service.ts'; export default module('my-app', ({ provision }) => { const quotes = provision(quotesService); provision(gatewayService, { deps: { quotes: quotes.rpc } }); }); ``` Next to it goes `prisma.config.ts`, the one config file the Prisma CLI reads. Composer's configuration is its `composer` section: the extensions that deploy your app and the store that keeps deploy state. Composer's commands read the `composer` section; your app code never imports the file: ```ts // prisma.config.ts import { defineConfig as composer } from '@prisma/composer/config'; import { nodeBuild } from '@prisma/composer/node/control'; import { prismaCloud, prismaState } from '@prisma/composer-prisma-cloud/control'; import { definePrismaConfig } from 'prisma/config'; export default definePrismaConfig({ composer: composer({ extensions: [prismaCloud(), nodeBuild()], state: prismaState(), }), }); ``` The commands find `prisma.config.ts` from the directory you run them in, so run them from the app's root. When you add a database, its `orm` section goes in this same file. ## 5. Run it locally One command brings the whole app up on your machine — both services wired together, no cloud credentials. Like deploy, it runs your *built* output, so build first (the build is §6, just below): ```sh pnpm build prisma-composer dev module.ts ``` It runs the same pipeline a deploy runs, against local stand-ins for Prisma Cloud, and prints the front door — each service's local URL: ``` [dev] ready: [dev] gateway http://localhost:3001 [dev] quotes http://localhost:3000 [dev] logs: prisma-composer log module.ts curl localhost:3001 # Make it work, make it right, make it fast. ``` `dev` keeps running and restarts a service when its build changes; `Ctrl-C` stops it (your data stays, so the next start is warm). It doesn't print service logs — that's a separate command so it doesn't bury the front door: ```sh prisma-composer log module.ts # every service, merged and prefixed prisma-composer log module.ts quotes # just one ``` Under the hood the local providers write the same `COMPOSER_*` environment variables a deploy writes, each keyed by the service's address — `COMPOSER_QUOTES_PORT` for the `quotes` service's own `port`, `COMPOSER_QUOTES_INPUT` for its input document, `COMPOSER_QUOTES_URL` for a `quotes` dependency's URL — so `service.load()`, `service.input()`, and `service.port()` read exactly what they'll read in production. You never set them by hand locally; `dev` *is* the deploy. Full workflow (one-service logs, `--tail`, `--fresh`, what persists) in [Running locally](running-locally.md). ## 6. Build and deploy You own the build — the framework only assembles what you built. It asks one thing of it: each entry must be a **single self-contained file**, with everything inlined except the runtime's own built-ins (`bun`, `bun:*`, `node:*`), which the deploy VM provides. Deploy copies that one file and never ships `node_modules`, so anything left un-inlined fails at boot. Any bundler that produces such a file works; this guide uses bun. Two services means two separate builds — not one multi-entry build, which would split the shared contract code into a chunk neither output contains: ```jsonc // package.json "scripts": { "build": "bun build src/quotes/server.ts --target=bun --outfile dist/quotes/server.mjs && bun build src/gateway/server.ts --target=bun --outfile dist/gateway/server.mjs" } ``` ```sh pnpm run build ``` Deploying needs exactly two environment variables. Create a service token in your workspace in the [Prisma Console](https://console.prisma.io); the workspace id is in the workspace's settings: ```sh export PRISMA_SERVICE_TOKEN=... export PRISMA_WORKSPACE_ID=... pnpm exec prisma-composer deploy module.ts ``` The CLI creates a Project named `my-app` in your workspace, provisions both services on Prisma Compute, points the gateway's `quotes` dependency at the deployed quotes service, and starts everything. The deploy finishes by printing what it made — your own module names, the platform resource each became, and the public URLs: ``` my-app ├─ quotes compute-service cps_abc123 │ https://xyz.ewr.prisma.build └─ gateway compute-service cps_def456 https://uvw.ewr.prisma.build ``` Open the gateway's URL from that output: you get a quote, served over one typed RPC hop. Now try the *quotes* service directly — `curl /rpc/random` — and you'll get `401`. That's deliberate. Deploying also gave the gateway a **service key** for its `quotes` dependency, and told quotes to accept only that: quotes answers the gateway and turns away everyone else. Neither service's code mentions a key, and it's why the local run needed none — only a deploy creates them. [Building an app](building-an-app.md#calls-are-authenticated-for-you) has the details. Two more things the generated client does for you, both invisible in your code: every call carries an idempotency key and retries safely if the target was still cold-starting, and the provider deduplicates on that key so a retry never runs your handler twice. (A hand-rolled request without a key still works — it just isn't deduplicated.) [Building an app](building-an-app.md#calls-retry-safely-for-you) has these too. Re-deploying is idempotent — it updates the same Project. For an isolated copy of the whole app (own services, own config), deploy a **stage**, and tear it down when you're done: ```sh pnpm exec prisma-composer deploy module.ts --stage demo pnpm exec prisma-composer destroy module.ts --stage demo ``` ## Porting an existing app You don't rewrite an app to put it on Composer — you declare it. The server code you already have stays the server; you add the declaration around it. **A Node/Bun service.** Add a `service.ts` with `compute({ name, deps, build: node({ module, entry }) })` pointing `entry` at your built server file, then make three changes to the server itself: 1. Read the port from `service.port()` (never `process.env`), and bind `0.0.0.0`. 2. Replace every other `process.env` read with what it really is: a field of the service's input schema for config and credentials, or a dependency for anything another service provides. If a value differs per stage — an app origin, an external URL — bind it with `envParam`; a credential, with `envSecret`. [Building an app § Service input](building-an-app.md#service-input) has the how-to-choose table and both shapes. 3. If it talks to Postgres: declare `deps: { db: rawPostgres() }` and build your existing client (`pg`, Bun's `SQL`, whatever you use today) from the injected `db.url` instead of a connection-string env var. Your build must produce a self-contained entry file — keep your own build if it already does. **A Next.js app.** Use the `nextjs` build adapter instead of `node`; `next build` with `output: 'standalone'` is the whole build: ```ts export default compute({ name: 'web', deps: { api: rpc(apiContract) }, build: nextjs({ module: import.meta.url, appDir: '..' }), }); ``` Add `nextjsBuild()` from `@prisma/composer/nextjs/control` to the deploy config's `extensions`. Any page or server action that calls `service.load()` needs `export const dynamic = 'force-dynamic'`, because the runtime environment doesn't exist at build time. [`examples/storefront-auth`](../../examples/storefront-auth/) is a complete ported-shaped app: a Next.js frontend calling a Bun API service that owns a Postgres. **More than one service.** Port them into one `module.ts` and replace the URLs they used to reach each other with contracts — that's the payoff: the edges become typed, and every environment (production, stages, tests) gets the wiring for free. ## Where to go next - [Building an app](building-an-app.md) — databases (including Prisma Next-typed ones with migrations), reusable Modules, cron/storage/streams, config params, secrets. - [Testing](testing.md) — unit tests with `mockService`, integration tests with `bootstrapService`. - [Running locally](running-locally.md) — `prisma-composer dev` and `prisma-composer log` in full: one service, `--tail`, `--fresh`, warm restarts. - [Deploying and operating](deploying.md) — stages, destroy, CI, how the app behaves in production. - [`examples/`](../../examples/) — complete apps: start with [orm-demo](../../examples/orm-demo/) (one service + one Prisma Next-typed database) or [store](../../examples/store/) (four modules, cron, a Next.js storefront).