Deploying
Deploy a Prisma App to production or an isolated stage, run it in CI, and tear environments down safely.
deploy takes your entry file (the one whose default export is the root module) and stands the whole app up on Prisma Compute and Prisma Postgres. You choose the target environment, production or an isolated stage, on the command line rather than in your code. Tearing an environment down is the destroy operation of the control API.
| You want to... | Run |
|---|---|
| Deploy to production | npx prisma@latest deploy module.ts |
| Deploy an isolated environment | npx prisma@latest deploy module.ts --stage <name> |
| Deploy under a different app name | npx prisma@latest deploy module.ts --name demo-42 |
Credentials
On your machine, deploy uses the session stored by auth login, the same sign-in every other CLI command uses:
bunx prisma@latest auth loginIn CI or any headless environment, use a service token instead. Set two environment variables:
PRISMA_SERVICE_TOKEN: create a service token for your workspace in the Prisma Console.PRISMA_WORKSPACE_ID: in the workspace's settings.
A fresh checkout with just those two variables set deploys successfully. The CLI finds or creates everything else. Keep the values out of the repo (an .env you source at deploy time, or CI secrets). When PRISMA_SERVICE_TOKEN is set it takes priority over any stored session.
Build first
deploy does not build for you. It assembles what your build produced:
bun run build
bunx prisma@latest deploy module.tsDeploy state records what is already provisioned, so a re-deploy applies the difference instead of recreating everything. The platform stores this state with the environment it describes, scoped to that environment's branch inside the app's project. It is not stored on your machine, so your laptop and CI see the same state. Concurrent deploys of the same environment take turns through a lease on that state. While one deploy holds the lease, a second deploy fails immediately with a message naming the holder. If a deploy crashes, its lease expires after about a minute, and the next deploy takes over.
Production and stages
Deploying with no --stage targets production. In platform terms, the app is a project (named after your root module), and production lives at the project level. The name is looked up in your workspace: a project this module deployed before is reused, and redeploying converges it. A project with the same name whose hosted state the CLI cannot identify or verify (one created outside this module's deploys, for example) stops the command with HostedStateBootstrapError. Use --name to deploy under a different name, or rename the module.
--stage <name> deploys a complete, isolated copy of the app as a preview branch of that same project: its own services, its own databases, its own configuration. The only thing a stage shares with production is the code. Composer's stage and Compute's preview branch are the same object seen from two tools, which is why the stage's variables are the branch's environment variables:
bunx prisma@latest deploy module.ts # production
bunx prisma@latest deploy module.ts --stage staging # a persistent staging environment
bunx prisma@latest deploy module.ts --stage pr-42 # one environment per PRRe-deploying any environment is idempotent: it updates the resources in place. A stage name must be a valid git ref name. An invalid name fails the deploy rather than being renamed.
What a deploy prints
A deploy ends by printing what it made, in your own terms: the names you authored, what each one became on the platform, and the public URLs:
storefront-auth
├─ auth
│ └─ api compute-service cps_abc123
│ https://xyz.ewr.prisma.build
├─ db postgres-database db_def456
└─ web compute-service cps_ghi789
https://uvw.ewr.prisma.buildThe tree is your module structure: auth.api is the api service inside the auth Module. Under each name is the platform resource it became and its ID, the thing to search for in the Console when you need it.
URLs appear for publicly reachable endpoints. That is why a Compute service prints one and a database does not: a database has a connection string, not a public endpoint. Secrets are left out entirely, so a node whose only output is a keypair prints no resource line. A node that deployed but published nothing reportable still appears in the tree, marked (no entities reported).
Removing resources
Your module is the source of truth for what exists. To remove a resource, delete its provision from the code and deploy again: the deploy compares the declaration against the environment's deploy state and deletes whatever is no longer declared, alongside any creates and updates the same deploy needs.
Plan: 3 to update, 4 to delete, 1 to noop
[web2-deploy] deleted
[COMPOSER_WEB2_PORT-var] deleted
[COMPOSER_WEB2_ORIGIN-var] deleted
[web2-svc] deletedA removed service takes its deployment and its wiring with it, and the resources that remain are untouched: a two-service app whose second service is deleted from the module keeps serving from the first without interruption.
Two things a deploy never removes:
- The last service. An app is at least one service; a module that provisions nothing fails with
ASSEMBLE.SERVICE_MISSINGbefore anything runs, so you cannot empty an environment by emptying the module. - The project and its branches. Those are the containers an environment deploys into: created if absent on deploy, and never deleted by one. Tearing down a whole environment is the
destroyoperation; deleting an entire project isproject delete, which asks you to repeat the project id with--confirm <project-id>because it destroys the project's databases too.
Destroying
The unified CLI has no destroy command. Tear an environment down with the destroy operation from @prisma/composer/control (see Driving deploys from code). It requires an explicit target, { kind: 'stage', stage } or { kind: 'production' }, and there is no default, so a teardown always names what it destroys. Unlike the CLI, the control API does not read the session stored by auth login. Export PRISMA_SERVICE_TOKEN and PRISMA_WORKSPACE_ID (see Credentials) before running the script, or it stops with environment variable PRISMA_WORKSPACE_ID is required:
import { destroy } from '@prisma/composer/control';
const result = await destroy({ entry: 'module.ts', target: { kind: 'stage', stage: 'staging' } });
if (!result.ok) throw new Error(`${result.failure.code}: ${result.failure.message}`);If you deployed under a different name with --name, pass the same name here so the operation finds the right project.
Destroying a stage removes its resources, then deletes its branch along with the stage's deploy state. Destroying production removes the resources and, if the project is empty afterwards, deletes the project too, so empty projects do not pile up in your workspace. A project still holding another stage's resources stays in place. Tearing down a stage that was never deployed fails with "nothing deployed". It does not provision one first.
CI
On GitHub, connect the repository and add prisma/cloud-deploy-action to a workflow instead of scripting the deploy yourself. The action derives the target from the branch: the default branch deploys production, and every other branch deploys a stage named after it. Connected repositories authenticate through GitHub's OIDC tokens, without a stored secret. Deploy on push walks through the setup.
Any other CI runs the same commands as your machine: set the two credential variables as CI secrets, build, and deploy. The per-PR environment pattern:
bunx prisma@latest deploy module.ts --stage "pr-$PR_NUMBER" # on pushWhen the PR closes, tear the stage down with the destroy operation, targeting { kind: 'stage', stage: 'pr-<number>' } from a small script.
One extra step applies if your app binds input fields with envSecret or envParam (see Service input). Each stage stores its own copy of those platform variables. The deploying shell only seeds them. A fresh stage, such as a new pr-42, starts with none of them, so CI must export the values alongside the two credential variables. The first deploy then copies any missing values from the shell up to the stage. If a name is absent from both the platform and the shell, the deploy fails early and names the missing variable.
When a deploy stops on an effect version conflict
Before doing anything else, every Composer command verifies that the installed dependency tree gives the deploy engine the exact effect version @prisma/composer pins. When it does not, the command stops immediately:
Error: Dependency conflict: alchemy resolves effect@<found>, but
@prisma/composer requires effect@<required>.This happens when another dependency floats to a newer effect and your package manager hoists that copy where the deploy engine resolves it. The fix is what the error says: pin effect in your app's package.json to the exact version the error names, as shown in Getting started, and reinstall. The pin goes stale when you upgrade @prisma/composer; the error names the new version each time.
When a deploy stops on a missing connection value
A dependency's connection declares the values it needs, by name, and the node on the other end has to supply them. When one does not, the deploy stops and names the edge rather than standing the app up:
Connection input "auth.db" declares param "url", but its producer "db" did not
supply it — the producer's outputs carry [host].Fix it at whichever end is wrong: add the name to the outputs the producer returns, or mark the param optional on the connection if absent is a valid value (the consumer then reads undefined). Do not make the param optional just to route around the error: that reinstates a silent undefined. You will only meet this error if you authored the connection or an extension on one side of the wire. Every block that ships with the framework supplies what it declares.
Production behavior
Behavior you will run into once the app is deployed, and what to do about it:
-
Compute scales to zero, and idle database connections get closed. A long-lived client that treats a dropped connection as fatal will crash-loop through 502s. Keep the pool small and reconnect-friendly, and do not let an async error kill the process:
const sql = new SQL({ url: db.url, max: 1, idleTimeout: 10 }); process.on('uncaughtException', (err) => console.error('uncaughtException', err)); process.on('unhandledRejection', (err) => console.error('unhandledRejection', err)); -
Bind
0.0.0.0, not loopback. Compute routes external HTTP to the VM; alocalhostlistener is unreachable from outside. -
A deployed
/rpc/<method>returns401to you. Calls are authenticated for you, and yourcurlis not one of the services the app connected. Reach it through a consumer instead. -
Calls into a sleeping service can get
ECONNRESETwhile it cold-starts. Retry them; the generated RPC client already does. -
The
COMPOSER_*variables in your project belong to the deploy. Config, secret pointers, and service keys all land there, and every deploy rewrites them. An edit made by hand does not survive the next deploy. -
Streaming responses do not stream. The platform's HTTP front door buffers a response until it completes, so an open SSE tail delivers nothing and times out at 60 seconds. Do not build on streamed HTTP responses.
-
Next.js pages that call
service.load()needexport const dynamic = 'force-dynamic'. The runtime environment does not exist at build time, and Next.js will not re-read it for prerendered routes.
Driving deploys from code
Everything the CLI does is also callable in-process, from @prisma/composer/control: typed deploy, destroy, dev, and log operations that return structured results instead of printing and exiting. The operations and their result shapes are documented in the control API on the deploy command reference.
Next steps
- Local development: the same pipeline, no credentials.
- Service input: how per-stage configuration and secrets travel.
deploycommand reference: every flag, plus the control API for destroy and log.
