Skip to main content
Prisma Documentation Docs

Search documentation

Type to search this documentation.

On this pageOverview

Deploying

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 deploy module.ts
Deploy an isolated environment npx prisma deploy module.ts --stage <name>
Deploy under a different app name npx prisma deploy module.ts --name demo-42

On your machine, deploy uses the session stored by auth login, the same sign-in every other CLI command uses:

title="bun"
bunx prisma auth login
pnpm
pnpm prisma auth login
yarn
yarn prisma auth login
npm
npx prisma 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 those two variables set deploys successfully once the app's project exists. The very first deploy creates that project, and a new project needs a region: set it in the deploy config or in the environment, as described in the deploy reference. Once the project exists it keeps its region, and later deploys, stages included, inherit it. 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.

deploy does not build for you. It assembles what your build produced:

bun run build

bunx prisma deploy module.ts
Bash
pnpm run build
pnpm prisma deploy module.ts
Bash
yarn build
yarn prisma deploy module.ts
Bash
npm run build
npx prisma 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.

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.

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 deploy module.ts                  # production

bunx prisma deploy module.ts --stage staging  # a persistent staging environment

bunx prisma deploy module.ts --stage pr-42    # one environment per PR
Bash
pnpm prisma deploy module.ts                  # production
pnpm prisma deploy module.ts --stage staging  # a persistent staging environment
pnpm prisma deploy module.ts --stage pr-42    # one environment per PR
Bash
yarn prisma deploy module.ts                  # production
yarn prisma deploy module.ts --stage staging  # a persistent staging environment
yarn prisma deploy module.ts --stage pr-42    # one environment per PR
Bash
npx prisma deploy module.ts                  # production
npx prisma deploy module.ts --stage staging  # a persistent staging environment
npx prisma 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.

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.

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).

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.

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:

title="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.

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 deploy module.ts --stage "pr-$PR_NUMBER"    # on push
Bash
pnpm prisma deploy module.ts --stage "pr-$PR_NUMBER"    # on push
Bash
yarn prisma deploy module.ts --stage "pr-$PR_NUMBER"    # on push
Bash
npx prisma 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 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.

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 your app, or another dependency of it, pins a different effect than @prisma/composer does and your package manager hoists that copy where the deploy engine resolves it. A plain Composer app never hits it: @prisma/composer and @prisma/composer-prisma-cloud pin every effect-family package the engine would otherwise float, so a fresh install resolves a single effect. The fix is to use the same effect as Composer: match your own effect dependency to the exact version the error names (dependencies.effect in @prisma/composer), or, when a dependency you cannot change pins another version, force Composer's with an overrides entry for effect in your package.json (resolutions in Yarn, under "pnpm" for pnpm) and reinstall. The pin goes stale when you upgrade @prisma/composer; the error names the new version each time.

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.

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 cron scheduler stays awake on purpose. It holds Compute's keep-awake guard for its whole lifetime, so it never scales to zero: one warm instance per app is the cost of the clock. Every other service, the runner included, sleeps as usual.

  • 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 stream, but the first bytes have a 60-second deadline. Compute's ingress forwards your response as your service sends it; it does not hold it back until the response completes. What it bounds is the wait for the response to start: send nothing for 60 seconds and the client gets 504 Gateway Time-out and the request is cancelled. Send the first bytes early, keep data flowing rather than letting a started stream sit idle, and have a long-lived client reconnect instead of holding one connection open. See Request timeout.

  • 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.

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.

Suggest an edit

Propose a replacement for this page. The site team reviews it before applying any changes.

Export
Documentation menu