Prisma 8 is here.Read the docs

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 productionnpx prisma@latest deploy module.ts
Deploy an isolated environmentnpx prisma@latest deploy module.ts --stage <name>
Deploy under a different app namenpx 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 login

In 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.ts

Deploy 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 PR

Re-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.build

The 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] deleted

A 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:

  1. The last service. An app is at least one service; a module that provisions nothing fails with ASSEMBLE.SERVICE_MISSING before anything runs, so you cannot empty an environment by emptying the module.
  2. 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 destroy operation; deleting an entire project is project 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:

destroy-staging.ts
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 push

When 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; a localhost listener is unreachable from outside.

  • A deployed /rpc/<method> returns 401 to you. Calls are authenticated for you, and your curl is not one of the services the app connected. Reach it through a consumer instead.

  • Calls into a sleeping service can get ECONNRESET while 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() need export 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

On this page