> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sevalla.com/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> These are the docs for Sevalla, a platform for deploying applications, databases, static sites, object storage and load balancers. The dashboard is at https://app.sevalla.com.
> Bold text in steps is an exact dashboard label.
> To act on a Sevalla account, prefer the Sevalla MCP server (https://mcp.sevalla.com/mcp, see https://docs.sevalla.com/ai-agents/overview.md), then the Sevalla CLI (https://docs.sevalla.com/developer-tools/cli.md), then the REST API (https://api-docs.sevalla.com, OpenAPI spec at https://api.sevalla.com/v3/openapi.json).
> Every page index is at https://docs.sevalla.com/llms.txt.

# Deploy a RedwoodJS (CedarJS) application

> Deploy a RedwoodJS app to Sevalla with CedarJS, the maintained Redwood GraphQL framework: both sides in one web process, PostgreSQL with Prisma, and migrations in a job.

A RedwoodJS application runs on Sevalla as one Node.js application: Ingot, Sevalla's builder, runs `yarn build`, which builds the API side and the web side, and the web process runs `cedarjs-server`, which serves the web side on the port that Sevalla sets in the `PORT` environment variable and passes `/.api/functions` requests to the GraphQL API in the same instance. The data lives in a Sevalla PostgreSQL database that Prisma reaches over an internal connection, and a job applies the Prisma migrations before each deployment.

This guide uses CedarJS 7, the maintained continuation of the RedwoodJS GraphQL framework. The RedwoodJS team renamed that framework Redwood GraphQL, released its last version, 8.9.0, on Oct 21, 2025, and now builds RedwoodSDK, a different framework that runs only on Cloudflare. Redwood GraphQL 8.9.0 requires Node.js 20, which reached its end of life in April 2026, and `redwoodjs.com` now redirects to RedwoodSDK; the RedwoodJS maintainers point Redwood GraphQL users to the documentation at [cedarjs.com](https://cedarjs.com). CedarJS is a fork of Redwood GraphQL, maintained by one of its core contributors, and keeps Redwood's sides, cells, services and SDL files, with the `cedar` CLI in place of `rw`. To move an existing RedwoodJS application, follow [Migrating from RedwoodJS to CedarJS](https://cedarjs.com/docs/upgrade-guides/redwood-to-cedar).

## At a glance

| | |
| - | - |
| Source | A Git repository |
| Build | [Ingot](https://docs.sevalla.com/applications/builds/ingot.md) (Node.js 24, Yarn 4), or the [Dockerfile](#deploy-with-a-dockerfile) |
| Start command | `cedarjs-server`, from `scripts.start` in `package.json` |
| Port | The `PORT` environment variable, `8080` by default |
| Processes | Web process; `Migrate` job (`prisma migrate deploy --config api/prisma.config.cjs`, **Before deployment**) |
| Services | PostgreSQL 18 (`storefront-db`) over an internal connection |
| Disk | None |
| Health check | `GET /.api/functions/graphql/health` returns `200` (readiness probe) |
| Size | **H1** for the web process and the `Migrate` job, **DB1** or larger for the database |

### Environment variables

| Variable | Required | Available during | Secret | Value |
| - | - | - | - | - |
| `DATABASE_URL` | Yes | Runtime | Yes | `DB_URL` from the internal connection, renamed: `postgres://<user>:<password>@<host>:<port>/<database>` |
| `PORT` | No | Runtime | No | Set by Sevalla to the web process port |

The internal connection also adds `DB_HOST`, `DB_PORT`, `DB_DATABASE`, `DB_USERNAME` and `DB_PASSWORD`, which the application doesn't read.

## Before you begin

* A Sevalla company with a payment method, and a GitHub, GitLab or Bitbucket account for the repository. See [Git providers](https://docs.sevalla.com/git/overview.md).
* Node.js 24 on your computer, with Yarn enabled through Corepack: `corepack enable`. CedarJS 7 needs Node.js 24 or later; Node.js 24 still ships Corepack.
* PostgreSQL on your computer, to create migrations against. With Docker:

  ```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
  docker run -d --name storefront-db -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=storefront -p 5432:5432 postgres:18
  ```

## Create the project

Create a CedarJS application with TypeScript and install its dependencies:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
npx create-cedar-app@latest storefront --typescript --no-git-init --install
cd storefront
```

The project is a Yarn 4 workspace with two sides: `api/`, the GraphQL API with Prisma, and `web/`, the React application that Vite builds. `package.json` pins Node.js 24 in `engines.node` and Yarn 4.14.1 in `packageManager`, and its scripts are `build` (`cedar build`) and `start` (`cedarjs-server`).

### Switch Prisma to PostgreSQL

The new project stores its data in SQLite. Replace the SQLite driver with Prisma's PostgreSQL adapter, and make `prisma` a dependency instead of a devDependency, so the image keeps the CLI that the migration job runs:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
yarn workspace api remove @prisma/adapter-better-sqlite3 better-sqlite3
yarn workspace api add @prisma/adapter-pg@7.8.0 pg
yarn remove prisma
yarn add prisma@7.8.0
```

`@7.8.0` keeps the adapter and the CLI on the Prisma version that CedarJS 7.1 installs for `@prisma/client`.

In `api/db/schema.prisma`, set the provider to `postgresql` and replace the `UserExample` model with a `Comment` model:

```prisma api/db/schema.prisma theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
datasource db {
  provider = "postgresql"
}

generator client {
  provider               = "prisma-client"
  output                 = "./generated/prisma"
  moduleFormat           = "cjs"
  generatedFileExtension = "mts"
  importFileExtension    = "mts"
}

model Comment {
  id        Int      @id @default(autoincrement())
  name      String
  body      String
  createdAt DateTime @default(now())
}
```

Replace `api/src/lib/db.ts` so that Prisma connects through the PostgreSQL adapter, with the address in `DATABASE_URL`:

```ts api/src/lib/db.ts theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
import { PrismaPg } from '@prisma/adapter-pg'
import { PrismaClient } from 'api/db/generated/prisma/client.mts'

import { emitLogLevels, handlePrismaLogging } from '@cedarjs/api/logger'

import { logger } from './logger.js'

export * from 'api/db/generated/prisma/client.mts'

const adapter = new PrismaPg({
  connectionString: process.env.DATABASE_URL,
})

const prismaClient = new PrismaClient({
  log: emitLogLevels(['info', 'warn', 'error']),
  adapter,
})

handlePrismaLogging({
  db: prismaClient,
  logger,
  logLevels: ['info', 'warn', 'error'],
})

export const db = prismaClient
```

Point Prisma at the database on your computer in `.env`, which `.gitignore` keeps out of the repository, then create the first migration:

```bash .env theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/storefront
```

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
yarn cedar prisma migrate dev --name init
```

Prisma writes the SQL to `api/db/migrations/<timestamp>_init/migration.sql`. Commit the `api/db/migrations` folder: the migration job applies what's in it.

### Add the API and the page

Generate the GraphQL schema and the service for `Comment`, without the update and delete mutations:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
yarn cedar generate sdl Comment --no-crud --no-tests
```

Replace the two files it writes. Anyone can read and add comments, so the query and the mutation use `@skipAuth`, as the comments in Cedar's tutorial do:

```ts api/src/graphql/comments.sdl.ts theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
export const schema = gql`
  type Comment {
    id: Int!
    name: String!
    body: String!
    createdAt: DateTime!
  }

  type Query {
    comments: [Comment!]! @skipAuth
  }

  input CreateCommentInput {
    name: String!
    body: String!
  }

  type Mutation {
    createComment(input: CreateCommentInput!): Comment! @skipAuth
  }
`
```

```ts api/src/services/comments/comments.ts theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
import type { MutationResolvers, QueryResolvers } from 'types/graphql'

import { db } from 'src/lib/db'

export const comments: QueryResolvers['comments'] = () => {
  return db.comment.findMany({ orderBy: { id: 'asc' } })
}

export const createComment: MutationResolvers['createComment'] = ({
  input,
}) => {
  return db.comment.create({ data: input })
}
```

Generate a home page and a cell that lists the comments:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
yarn cedar generate page home / --no-tests --no-stories
yarn cedar generate cell Comments --no-tests --no-stories
```

```tsx web/src/components/CommentsCell/CommentsCell.tsx theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
import type { CommentsQuery, CommentsQueryVariables } from 'types/graphql'

import type {
  CellFailureProps,
  CellSuccessProps,
  TypedDocumentNode,
} from '@cedarjs/web'

export const QUERY: TypedDocumentNode<CommentsQuery, CommentsQueryVariables> =
  gql`
    query CommentsQuery {
      comments {
        id
        name
        body
      }
    }
  `

export const Loading = () => <p>Loading comments...</p>

export const Empty = () => <p>No comments yet.</p>

export const Failure = ({
  error,
}: CellFailureProps<CommentsQueryVariables>) => <p>Error: {error?.message}</p>

export const Success = ({
  comments,
}: CellSuccessProps<CommentsQuery, CommentsQueryVariables>) => (
  <ul>
    {comments.map((comment) => (
      <li key={comment.id}>
        <strong>{comment.name}</strong>: {comment.body}
      </li>
    ))}
  </ul>
)
```

```tsx web/src/pages/HomePage/HomePage.tsx theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
import { Metadata } from '@cedarjs/web'

import CommentsCell from 'src/components/CommentsCell'

const HomePage = () => (
  <>
    <Metadata title="Home" description="Comments from Storefront customers" />
    <h1>Storefront</h1>
    <CommentsCell />
  </>
)

export default HomePage
```

The web side is a single-page application: `web/index.html` holds no content until the JavaScript runs. Give it a title, so that the page has one before React renders. In `web/index.html`, add this line inside `<head>`:

```html web/index.html theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
<title>Storefront</title>
```

## Prepare the project for Sevalla

CedarJS already has what Sevalla needs:

| File | Why Sevalla needs it |
| - | - |
| `package.json` `engines.node` | [Ingot](https://docs.sevalla.com/applications/builds/ingot.md), the default build strategy, builds on the Node.js 24 line that `24.x` pins. See [Pin the Node.js version](https://docs.sevalla.com/guides/javascript/overview.md#pin-the-node-js-version). |
| `package.json` `packageManager` | Ingot installs Yarn 4.14.1 with Corepack and runs `yarn install --immutable`. |
| `yarn.lock` | The exact versions to install. Commit it. |
| `package.json` `scripts.build` and `scripts.start` | Ingot runs `yarn run build` during the build and starts the web process with the `start` script, `cedarjs-server`. |
| `cedar.toml` | `apiUrl = "/.api/functions"`: the web side calls the API on its own domain, and `cedarjs-server` passes those requests to the API server in the same instance. |
| `api/db/migrations` | The migrations that the `Migrate` job applies. |

### How Ingot builds and starts the application

* Ingot installs every dependency of both sides, then runs `yarn build`, which runs `cedar build`: it generates the Prisma client, builds the API side into `api/dist` and the web side into `web/dist`. `prisma generate` doesn't connect to the database, but `api/prisma.config.cjs` needs a `DATABASE_URL`: during the build, CedarJS loads the placeholder `DATABASE_URL=file:./db/dev.db` from `.env.defaults`, so keep that line. At runtime, the `DATABASE_URL` that Sevalla sets takes precedence over `.env.defaults`.
* After the build, Ingot removes the devDependencies with `yarn workspaces focus --all --production`. The image keeps `@cedarjs/api-server`, which provides `cedarjs-server`, and `prisma`, which you made a dependency.
* The web process runs `cedarjs-server`. It serves `web/dist` on [`PORT`](https://docs.sevalla.com/guides/javascript/overview#listen-on-the-port-environment-variable), which Sevalla sets (`8080` by default), on all network interfaces, starts the API server on port `8911` inside the instance, and passes requests for `/.api/functions` to it. The page and the API share the application's domain, so the browser needs no CORS setup.
* Ingot doesn't run migrations. A [job](https://docs.sevalla.com/applications/processes.md#jobs) runs `prisma migrate deploy` once per deployment, before the new version starts. The image has no Yarn 4, so the job runs the `prisma` command, not `yarn cedar prisma`.

`cedarjs-server` doesn't handle `SIGTERM`: when Sevalla stops an instance, Node.js exits at once, and a request still running on that instance fails.

Run `yarn cedar dev` and open `http://localhost:8910` to check the application locally; the API answers on `http://localhost:8911`. Then commit everything and push it to your Git provider. CedarJS's `.gitignore` keeps `.env`, `node_modules`, `dist` and the generated Prisma client out of the repository:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
git init
git add .
git commit -m "Prepare for Sevalla"
git branch -M main
git remote add origin https://github.com/acme/storefront.git
git push -u origin main
```

## Deploy to Sevalla

These steps build with [Ingot](https://docs.sevalla.com/applications/builds/ingot.md). To build from your own Dockerfile instead, see [Deploy with a Dockerfile](#deploy-with-a-dockerfile).

**1. Create the database**

In the sidebar, click **Databases**, then click **Create database**. Under **Database**, choose **PostgreSQL** and the newest version. Enter `storefront-db` as the **Display name**, choose a **Data center** and a **Size**, then click **Create database**. See [Create a database](https://docs.sevalla.com/databases/create.md) for every field.

**2. Create the application**

In the sidebar, click **Applications**, then click **Create** > **Application**. Under **Source**, click **Private repository**, then choose the **Git provider**, the **Repository** and the **Branch**. Under **Location**, choose the same **Data center** as the database. Keep the **Size** at **H1** or choose a larger one, then click **Create application**. The application opens on its **Overview** tab with the status **Not deployed yet**: creating an application doesn't deploy it.

**3. Connect the database**

Click the **Networking** tab. Under **Connected services**, click **Add internal connection**. Choose `storefront-db` as the **Service** and turn on **Add environment variables to the application**. In the list of variables, change the key `DB_URL` to `DATABASE_URL`, then click **Add internal connection**. See [Connect an application](https://docs.sevalla.com/databases/connect.md#connect-an-application).

**4. Create the migration job**

Click the **Processes** tab, then click **Create process**. Choose **Job**, enter `Migrate` as the **Name** and `prisma migrate deploy --config api/prisma.config.cjs` as the **Custom start command**. Keep the **Start policy** at **Before deployment** and the **Resource** at **H1**, then click **Create process**.

**5. Add a readiness probe**

On the **Processes** tab, click **⋯** on the web process, then click **Update process**. Under **Health checks**, turn on **Readiness probe** with the **Path** `/.api/functions/graphql/health`, then click **Update process**. New instances get traffic only once both sides answer. See [Health checks](https://docs.sevalla.com/applications/processes.md#health-checks).

**6. Deploy**

Click the **Overview** tab, then click **Deploy now** under **Recent deployments**. Check the **Branch** and click **Deploy**.

**7. Turn on auto-deploy**

To deploy every push to the branch automatically, click the **Settings** tab, then click **Update auto-deploy** in the **Auto-deploy** section. Turn on **Auto-deploy** and click **Update auto-deploy**. See [Deployments](https://docs.sevalla.com/applications/deployments.md#deploy-automatically-on-push).

![The Create process sheet with Job selected, the Name and Custom start command fields, the Start policy set to Before deployment, and the H1 resource](https://assets.sevalla.com/screenshots/light/app-processes-job-create.png)

## Verify the deployment

1. On the **Deployments** tab, wait for the deployment's status to change to **Deployed**. The application's status next to its name changes to **Running**. Open the deployment to read its build log. Ingot's [build plan](https://docs.sevalla.com/applications/builds/ingot.md#read-the-build-plan) comes first:

   ```text Build log theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
   ╭─ ■ Node.js 24 application ───────────────────────────────────╮
   │ Node.js          24              package.json engines "24.x" │
   │ Package manager  yarn 4.14.1     package.json packageManager │
   │ Start            cedarjs-server  package.json scripts.start  │
   │ DevDependencies  removed after the build                     │
   ├─ Image ──────────────────────────────────────────────────────┤
   │ Build            node:24-trixie                              │
   │ Runtime          node:24-trixie-slim                         │
   │ User             1000:1000                                   │
   │ Init             tini                                        │
   │ Port             $PORT, default 8080                         │
   ╰─────────────────────────────────────────────── Ingot v0.4.1 ─╯
   ```

   Later, during the rollout, the log shows `Migrate has been successful`. If the job fails, the deployment fails and nothing changes for visitors.
2. Click the domain under the application's name, for example `storefront-x7k2m.sevalla.app`. The browser tab shows the title `Home | Storefront`, and the page shows **Storefront** with the comments, or `No comments yet.` before the first one.
3. Check both sides from a terminal: the web side's title, then a comment created and read through the GraphQL API:

   ```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
   curl -s https://storefront-x7k2m.sevalla.app/ | grep -o '<title>.*</title>'
   curl -s https://storefront-x7k2m.sevalla.app/.api/functions/graphql \
     -H "Content-Type: application/json" \
     -d '{"query":"mutation { createComment(input: {name: \"Ada\", body: \"Fast shipping\"}) { name body } }"}' \
     | jq -c .data
   curl -s https://storefront-x7k2m.sevalla.app/.api/functions/graphql \
     -H "Content-Type: application/json" \
     -d '{"query":"{ comments { name body } }"}' \
     | jq -c '.data.comments | last'
   ```

   ```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
   <title>Storefront</title>
   {"createComment":{"name":"Ada","body":"Fast shipping"}}
   {"name":"Ada","body":"Fast shipping"}
   ```

   Reload the page in the browser: it lists `Ada: Fast shipping`.

With the API, [Get deployment](https://api-docs.sevalla.com/v3/applications/get-deployment) (`getDeploymentV3`) returns the deployment's `status`, and [Get application deployment logs](https://api-docs.sevalla.com/v3/applications/get-application-deployment-logs) (`getApplicationDeploymentLogsV3`) returns its build log. See [Deploy with the API](#deploy-with-the-api).

## Deploy with a Dockerfile

Ingot needs no Dockerfile. Write your own when you want full control of the image: the base image, the system packages and every layer. Add a `Dockerfile` and a `.dockerignore` to the root of the repository:

```dockerfile Dockerfile theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
FROM node:24-trixie-slim AS build
WORKDIR /app
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0
RUN corepack enable
COPY package.json yarn.lock .yarnrc.yml ./
COPY api/package.json api/
COPY web/package.json web/
RUN yarn install --immutable
COPY . .
RUN yarn cedar build && yarn workspaces focus --all --production

FROM node:24-trixie-slim
# hadolint ignore=DL3008
RUN apt-get update \
  && apt-get install -y --no-install-recommends tini \
  && rm -rf /var/lib/apt/lists/*
WORKDIR /app
ENV NODE_ENV=production PORT=8080 PATH=/app/node_modules/.bin:$PATH
COPY --from=build /app ./
USER 1000:1000
EXPOSE 8080
ENTRYPOINT ["tini", "--"]
CMD ["cedarjs-server"]
```

| Line or choice | Why |
| - | - |
| `node:24-trixie-slim` | The official Node.js 24 image on Debian 13, the line `engines.node` pins. The tag follows Node.js 24's patch releases, so each build gets its security fixes; a digest would stay on one release. It has the OpenSSL 3 library that Prisma's migration engine needs. |
| `corepack enable` | Installs the Yarn release that `packageManager` names, 4.14.1, for the build stage only. |
| Manifests and `yarn.lock` first | `yarn install --immutable` installs the exact versions in the lock file and fails if it's out of date. Docker reuses this layer while the four files don't change, so a code change skips the install. |
| `yarn cedar build`, then `yarn workspaces focus --all --production` | Builds both sides and generates the Prisma client, then removes the devDependencies. `cedarjs-server` and `prisma` stay. |
| `tini` | Runs as PID 1 and passes `SIGTERM` on to Node.js. It comes from Debian's packages, unpinned so that rebuilds get its fixes, which is why the line before it tells hadolint to skip its version-pinning check. |
| `PATH` | Puts `node_modules/.bin` first, so the `Migrate` job's `prisma` command works as it does in Ingot's image. |
| `USER 1000:1000` | The image's unprivileged `node` user. The files stay owned by root, so the server can read them but not change them. |
| `PORT=8080` and `EXPOSE 8080` | `cedarjs-server` listens on `PORT`, which Sevalla sets at runtime. The image's default matches Sevalla's, for running it on your computer. |

```text .dockerignore theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
.git
**/node_modules
**/dist
api/db/generated
.cedar
.env
.env.*
!.env.defaults
*.log
Dockerfile
.dockerignore
```

The `.dockerignore` keeps your local `.env` out of the image, but not `.env.defaults`, which the build needs for its placeholder `DATABASE_URL`.

Commit and push both files. Ingot doesn't use the `Dockerfile`, so you can keep it in a repository that builds with Ingot, but it adds the `.dockerignore` patterns to its own: its build plan then shows `Ignored 11 patterns from .dockerignore`, and the Ingot build still passes. Then switch the application to the Dockerfile build strategy:

**1. Choose the Dockerfile build strategy**

On the application's **Settings** tab, in the **Build strategy** section, click **Update build strategy**. Click **Dockerfile**, keep the **Dockerfile path** `Dockerfile` and the **Docker context** `.`, then click **Update build strategy**.

**2. Deploy**

Click the **Overview** tab, then click **Deploy now** under **Recent deployments**. Check the **Branch** and click **Deploy**.

The deployment's build log shows `📦 Building and pushing docker image using Dockerfile...` instead of Ingot's build plan, then `Migrate has been successful`, and its **Dockerfile** tab shows your file. The `Migrate` job and `DATABASE_URL` stay as they are. When the deployment's status is **Deployed**, the checks in [Verify the deployment](#verify-the-deployment) print the same lines, with the comments created so far. See [Dockerfile](https://docs.sevalla.com/applications/builds/dockerfile.md).

## Deploy changes

With **Auto-deploy** on, every push to the branch starts a deployment; otherwise, click **Deploy now** for each change. Each deployment builds the image, runs the `Migrate` job, then replaces the web process's instances. To change the schema, edit `api/db/schema.prisma`, run `yarn cedar prisma migrate dev --name <change>` on your computer, and commit the new folder in `api/db/migrations` with the code that uses it. See [Deployments](https://docs.sevalla.com/applications/deployments.md).

## Use environment variables

The API side reads every environment variable from `process.env`. The web side sees only the variables that `cedar.toml` lists in `includeEnvironmentVariables`, or whose names start with `REDWOOD_ENV_`. Vite writes their values into the JavaScript during the build, so those variables must be available during the build, and anyone can read them in the browser. Add variables on the application's **Environment variables** tab; they apply at the next deployment. See [Environment variables](https://docs.sevalla.com/applications/environment-variables.md).

## Add a custom domain

To serve the application from your own domain, such as `shop.example.com`, add the domain on the application's **Domains** tab and create the DNS records that Sevalla shows. Custom domains need a web process larger than **H1**, so [scale](https://docs.sevalla.com/applications/scaling.md) the web process first. The web side calls the API on the same domain, so nothing else changes. See [Custom domains](https://docs.sevalla.com/domains/overview.md).

## Troubleshoot

| Symptom | Cause | Fix |
| - | - | - |
| The deployment fails at `Migrate has failed`, and the job's logs show `error This project's package.json defines "packageManager": "yarn@4.14.1". However the current global version of Yarn is 1.22.22.` | The job runs `yarn cedar prisma migrate deploy`. The image has the Yarn 1 that comes with the Node.js image, not the Yarn 4 that `packageManager` names. | Set the job's **Custom start command** to `prisma migrate deploy --config api/prisma.config.cjs`, then deploy. |
| The build fails while it generates the Prisma client, with `PrismaConfigEnvError: Cannot resolve environment variable: DATABASE_URL.` | `.env.defaults` is missing, or has no `DATABASE_URL` line, so `api/prisma.config.cjs` has no value during the build | Restore the line `DATABASE_URL=file:./db/dev.db` in `.env.defaults`, and keep `.env.defaults` out of `.dockerignore`. |
| The `Migrate` job fails with `PrismaConfigEnvError: Cannot resolve environment variable: DATABASE_URL.` | `DATABASE_URL` isn't set, or isn't available during runtime. `api/prisma.config.cjs` reads it. | Rename `DB_URL` to `DATABASE_URL` on the **Environment variables** tab, or add the internal connection with its variables, then deploy. |
| GraphQL requests return `Something went wrong.`, and the runtime logs show ``The table `public.Comment` does not exist in the current database.`` | No migration ran: the deployment has no `Migrate` job, or `api/db/migrations` isn't in the repository | Create the job as in [Deploy to Sevalla](#deploy-to-sevalla), commit `api/db/migrations`, then deploy. |

For more causes, see [Troubleshoot Ingot builds](https://docs.sevalla.com/applications/builds/ingot.md#troubleshoot-ingot-builds) and [Troubleshoot builds and deployments](https://docs.sevalla.com/applications/troubleshooting/builds-and-deployments.md).

## Deploy with the API

The [Sevalla API](https://docs.sevalla.com/developer-tools/api.md) makes the same deployment as [Deploy to Sevalla](#deploy-to-sevalla) without the dashboard. Run the steps in order in one shell: each step is one API call, and saves the IDs that later steps need in shell variables. Each step also names the [CLI](https://docs.sevalla.com/developer-tools/cli.md) command and the [MCP server](https://docs.sevalla.com/ai-agents/overview.md) tool that do the same. A variable that holds `null` means the call failed: run the `curl` command without its `jq` part to read the error.

You need:

* An [API key](https://docs.sevalla.com/developer-tools/api-keys.md) with the **Company developer** role, or the `APP:CREATE`, `APP:READ`, `APP:UPDATE`, `DATABASE:CREATE`, `DATABASE:READ` and `DATABASE:UPDATE` permissions, in the `SEVALLA_API_KEY` environment variable. The CLI reads the key from `SEVALLA_API_TOKEN`.
* The repository on GitHub, GitLab or Bitbucket, and a Git account that can read it connected to your Sevalla user, which only the dashboard can do. See [Git providers](https://docs.sevalla.com/git/overview.md). Creating the application checks the access: if Sevalla can't read the repository, the call returns `400` with `Cannot access repository https://github.com/acme/storefront: Repository not found or you don't have access.`, followed by where to grant the access.
* `openssl`, to generate the database password, and `curl` and `jq`.

The sequence creates two services, an application and a database. The API allows 20 creations of each per hour per company.

**1. Find the location**

[List clusters](https://api-docs.sevalla.com/v3/resources/list-clusters) (`listClustersV3`) returns every location with its `id`, its name in `display_name` and its region ID in `location`. Pick one by its region ID, here `eu-frankfurt-1` for Frankfurt, Germany. [Locations](https://docs.sevalla.com/platform/locations.md) lists them all.

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
CLUSTER_ID=$(curl -s https://api.sevalla.com/v3/resources/clusters \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  | jq -r '.[] | select(.location == "eu-frankfurt-1") | .id')
```

CLI: `sevalla resources clusters`. MCP: `get_platform_options`.

**2. Find the database size**

[List database resource types](https://api-docs.sevalla.com/v3/resources/list-database-resource-types) (`listDatabaseResourceTypesV3`) returns the ID of each database size. Pick **DB1**, the smallest, or a larger one:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
DB_SIZE_ID=$(curl -s https://api.sevalla.com/v3/resources/database-resource-types \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  | jq -r '.[] | select(.name == "db1") | .id')
```

CLI: `sevalla resources db-types`. MCP: `get_platform_options`.

**3. Find the newest PostgreSQL version**

[List database versions](https://api-docs.sevalla.com/v3/resources/list-database-versions) (`listDatabaseVersionsV3`) returns the versions the location offers, oldest first:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
PG_VERSION=$(curl -s "https://api.sevalla.com/v3/resources/database-versions?cluster_id=$CLUSTER_ID&type=postgresql" \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  | jq -r '.[0].versions | last')
echo "$PG_VERSION"
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
18
```

CLI: `sevalla resources db-versions --cluster "$CLUSTER_ID" --type postgresql`. MCP: `create_database` picks the newest version when you leave out `version`.

**4. Create the database**

[Create database](https://api-docs.sevalla.com/v3/databases/create-database) (`createDatabaseV3`) creates `storefront-db` with the database `storefront`, the user `storefront` and a password you generate. The API doesn't return the password later, and `DATABASE_URL` needs it, so keep it in `DB_PASSWORD`:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
DB_PASSWORD=$(openssl rand -hex 16)
DB_ID=$(curl -s -X POST https://api.sevalla.com/v3/databases \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d @- <<EOF | jq -r .id
{
  "display_name": "storefront-db",
  "type": "postgresql",
  "version": "$PG_VERSION",
  "cluster_id": "$CLUSTER_ID",
  "resource_type_id": "$DB_SIZE_ID",
  "db_name": "storefront",
  "db_user": "storefront",
  "db_password": "$DB_PASSWORD"
}
EOF
)
```

CLI: `sevalla databases create --name storefront-db --type postgresql --db-version "$PG_VERSION" --cluster "$CLUSTER_ID" --resource-type "$DB_SIZE_ID" --db-name storefront --db-user storefront --db-password "$DB_PASSWORD"`. MCP: `create_database` generates the password itself and never returns it. With `connect_to_application_id` and `env_var_name` set to `DATABASE_URL`, it also connects the database and adds `DATABASE_URL` as `postgresql://...`, which Prisma reads like `postgres://`; it replaces the **Connect the database** and **Add DATABASE\_URL** steps, and needs the application first.

**5. Wait for the database**

[Get database](https://api-docs.sevalla.com/v3/databases/get-database) (`getDatabaseV3`) returns the database's `status`, `creating` until it's `ready`, and its internal hostname and port, which `DATABASE_URL` needs. Poll it every 10 seconds, for up to 10 minutes:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
for i in $(seq 1 60); do
  DB=$(curl -s "https://api.sevalla.com/v3/databases/$DB_ID" -H "Authorization: Bearer $SEVALLA_API_KEY")
  STATUS=$(echo "$DB" | jq -r '.status // empty')
  [ "$STATUS" = ready ] && break
  sleep 10
done
echo "$STATUS"
DB_HOST=$(echo "$DB" | jq -r .internal_hostname)
DB_PORT=$(echo "$DB" | jq -r .internal_port)
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
ready
```

`internal_port` is a string, such as `"5432"`. CLI: `sevalla databases get "$DB_ID"`. MCP: `get_database`.

**6. Create the application**

[Create application](https://api-docs.sevalla.com/v3/applications/create-application) (`createApplicationV3`) creates the application with the Ingot build strategy and a web process, and doesn't deploy it.

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
APP_ID=$(curl -s -X POST https://api.sevalla.com/v3/applications \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d @- <<EOF | jq -r .id
{
  "display_name": "storefront",
  "cluster_id": "$CLUSTER_ID",
  "source": "privateGit",
  "git_type": "github",
  "repo_url": "https://github.com/acme/storefront",
  "default_branch": "main"
}
EOF
)
```

CLI: `sevalla apps create --name storefront --cluster "$CLUSTER_ID" --source privateGit --git-type github --repository https://github.com/acme/storefront --branch main`. MCP: `create_application` with `deploy` set to `false`.

**7. Find the web process**

[List processes](https://api-docs.sevalla.com/v3/applications/list-processes) (`listProcessesV3`) returns the web process that Create application added:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
WEB_ID=$(curl -s "https://api.sevalla.com/v3/applications/$APP_ID/processes" \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  | jq -r '.data[] | select(.type == "web") | .id')
```

CLI: `sevalla apps processes list --app-id "$APP_ID"`. MCP: `get_application`, in `processes`.

**8. Find the H1 size**

The API creates the web process with the **S1** size, where the dashboard picks **H1**. [List process resource types](https://api-docs.sevalla.com/v3/resources/list-process-resource-types) (`listProcessResourceTypesV3`) returns the ID of each size:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
H1_ID=$(curl -s https://api.sevalla.com/v3/resources/process-resource-types \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  | jq -r '.[] | select(.name == "h1") | .id')
```

CLI: `sevalla resources process-types`. MCP: `get_platform_options`.

**9. Set the web process size and readiness probe**

[Update process](https://api-docs.sevalla.com/v3/applications/update-process) (`updateProcessV3`) sets the size and adds the readiness probe from [Deploy to Sevalla](#deploy-to-sevalla), so a new instance gets requests only once both sides answer. The probe's other values are the dashboard's defaults. Send `timeoutSeconds`: a probe set through the API without it waits 120 seconds for an answer, not the dashboard's 1.

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s -X PATCH "https://api.sevalla.com/v3/applications/$APP_ID/processes/$WEB_ID" \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d @- <<EOF | jq -c '{size: .resource_type_name, readiness: .readiness_probe.httpGet.path}'
{
  "resource_type_id": "$H1_ID",
  "readiness_probe": {
    "httpGet": {"path": "/.api/functions/graphql/health", "port": 8080},
    "initialDelaySeconds": 0,
    "periodSeconds": 10,
    "timeoutSeconds": 1,
    "successThreshold": 1,
    "failureThreshold": 3
  }
}
EOF
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
{"size":"h1","readiness":"/.api/functions/graphql/health"}
```

CLI: `sevalla apps processes update "$WEB_ID" --app-id "$APP_ID" --resource-type "$H1_ID"` sets the size; the CLI has no option for probes. MCP: `sevalla_api_update` with `updateProcessV3` and the same body.

**10. Connect the database**

[Create internal connection](https://api-docs.sevalla.com/v3/applications/create-internal-connection) (`createApplicationInternalConnectionV3`) lets the application reach `storefront-db` over the private network. Unlike the dashboard, it adds no environment variables: the next step adds `DATABASE_URL`.

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s -X POST "https://api.sevalla.com/v3/applications/$APP_ID/internal-connections" \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"target_id\":\"$DB_ID\",\"target_type\":\"database\"}" \
  | jq -r 'has("id")'
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
true
```

CLI: `sevalla apps internal-connections create "$APP_ID" --target-id "$DB_ID" --target-type database`. MCP: `sevalla_api_create` with `createApplicationInternalConnectionV3` and the same body.

**11. Add DATABASE\_URL**

[Create environment variable](https://api-docs.sevalla.com/v3/applications/create-environment-variable) (`createEnvironmentVariableV3`) adds `DATABASE_URL` in the format the dashboard uses for `DB_URL`. The server and the `Migrate` job read it when they start, and the build doesn't need it, so it's available during runtime only:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s -X POST "https://api.sevalla.com/v3/applications/$APP_ID/env-vars" \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d "$(jq -n --arg value "postgres://storefront:$DB_PASSWORD@$DB_HOST:$DB_PORT/storefront" \
    '{key: "DATABASE_URL", value: $value, is_runtime: true, is_buildtime: false}')" \
  | jq -c '{key, is_runtime, is_buildtime}'
```

```json theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
{"key":"DATABASE_URL","is_runtime":true,"is_buildtime":false}
```

CLI: `sevalla apps env-vars create "$APP_ID" --key DATABASE_URL --value "postgres://..."`, which makes the variable available during the build too. MCP: `set_environment_variables` with `runtime` set to `true` and `buildtime` set to `false`.

**12. Create the migration job**

[Create process](https://api-docs.sevalla.com/v3/applications/create-process) (`createProcessV3`) adds the `Migrate` job, which applies the Prisma migrations before each deployment. The API needs the size in `resource_type_id`, the `scaling_strategy`, and the `job_start_policy` that the dashboard calls **Before deployment**:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s -X POST "https://api.sevalla.com/v3/applications/$APP_ID/processes" \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d @- <<EOF | jq -r '.display_name, .job_start_policy'
{
  "display_name": "Migrate",
  "type": "job",
  "entrypoint": "prisma migrate deploy --config api/prisma.config.cjs",
  "resource_type_id": "$H1_ID",
  "scaling_strategy": {"type": "manual", "config": {"instanceCount": 1}},
  "job_start_policy": "beforeDeployment"
}
EOF
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
Migrate
beforeDeployment
```

CLI: `sevalla apps processes create "$APP_ID" --name Migrate --type job --resource-type "$H1_ID" --command "prisma migrate deploy --config api/prisma.config.cjs" --job-start-policy beforeDeployment`. MCP: `sevalla_api_create` with `createProcessV3` and the same body.

**13. Turn on auto-deploy**

[Update application](https://api-docs.sevalla.com/v3/applications/update-application) (`updateApplicationV3`) with `auto_deploy` deploys every push to the branch:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s -X PATCH "https://api.sevalla.com/v3/applications/$APP_ID" \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"auto_deploy":true}' \
  | jq -r .auto_deploy
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
true
```

CLI: `sevalla apps update "$APP_ID" --data '{"auto_deploy":true}'`. MCP: `sevalla_api_update` with `updateApplicationV3`, the application ID as `path_params.id` and the same body.

**14. Deploy**

[Trigger deployment](https://api-docs.sevalla.com/v3/applications/trigger-deployment) (`triggerDeploymentV3`) deploys the latest commit of the branch:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
DEPLOYMENT_ID=$(curl -s -X POST "https://api.sevalla.com/v3/applications/$APP_ID/deployments" \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}' \
  | jq -r .id)
```

CLI: `sevalla apps deployments trigger "$APP_ID"`. MCP: `trigger_deployment`.

**15. Wait for the deployment**

[Get deployment](https://api-docs.sevalla.com/v3/applications/get-deployment) (`getDeploymentV3`) returns the deployment's `status`: `waiting`, `inProgress`, then `success`, `failed`, `cancelled` or `skipped`. Poll it until the deployment ends:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
for i in $(seq 1 120); do
  STATUS=$(curl -s "https://api.sevalla.com/v3/applications/$APP_ID/deployments/$DEPLOYMENT_ID" \
    -H "Authorization: Bearer $SEVALLA_API_KEY" | jq -r '.status // empty')
  case $STATUS in success|failed|cancelled|skipped) break ;; esac
  sleep 15
done
echo "$STATUS"
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
success
```

The loop asks every 15 seconds, for up to 30 minutes, and keeps asking through an empty or failed reply. CLI: `sevalla apps deployments get "$DEPLOYMENT_ID" --app-id "$APP_ID"`. MCP: `get_deployment`.

**16. Read the build log**

[Get application deployment logs](https://api-docs.sevalla.com/v3/applications/get-application-deployment-logs) (`getApplicationDeploymentLogsV3`) returns the build log, one line per entry. With Ingot, the `Start` row of the build plan shows the start command, and the rollout reports the `Migrate` job. Without `limit`, the call returns the latest 1,000 lines; `5000` is the most it allows:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s "https://api.sevalla.com/v3/applications/$APP_ID/deployments/$DEPLOYMENT_ID/logs?limit=5000" \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  | jq -r '.[].message' | grep -o -e '│ Start.*' -e 'Migrate has [a-z ]*' | tr -s ' '
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
│ Start cedarjs-server package.json scripts.start │
Migrate has been successful
```

With a Dockerfile, the log shows `📦 Building and pushing docker image using Dockerfile...` in place of the build plan. When the job fails, the log ends with `Migrate has failed`, the deployment fails, and the runtime logs hold the job's error. CLI: `sevalla apps deployments logs "$DEPLOYMENT_ID" --app-id "$APP_ID"`. MCP: `get_deployment_logs`.

**17. Read the Migrate job output**

[Get application runtime logs](https://api-docs.sevalla.com/v3/applications/get-application-runtime-logs) (`getApplicationRuntimeLogsV3`) returns what the processes print, the `Migrate` job included. `prisma migrate deploy` ends with `All migrations have been successfully applied.`, or `No pending migrations to apply.` when the database is up to date:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s "https://api.sevalla.com/v3/applications/$APP_ID/runtime-logs?limit=500" \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  | jq -r '.[].message' | grep -o -e 'All migrations have been successfully applied.' -e 'No pending migrations to apply.' | tail -1
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
All migrations have been successfully applied.
```

CLI: `sevalla apps logs runtime "$APP_ID" --search "migrations"`. MCP: `search_runtime_logs` with `search` set to `migrations`.

**18. Check the application**

[List application domains](https://api-docs.sevalla.com/v3/applications/list-application-domains) (`listApplicationDomainsV3`) returns the application's domain. Request the web side's title, then create and read a comment through the GraphQL API:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
for i in $(seq 1 30); do
  DOMAIN=$(curl -s "https://api.sevalla.com/v3/applications/$APP_ID/domains" \
    -H "Authorization: Bearer $SEVALLA_API_KEY" \
    | jq -r '.data[] | select(.is_primary) | .name')
  [ -n "$DOMAIN" ] && break
  sleep 2
done
curl -s "https://$DOMAIN/" | grep -o '<title>.*</title>'
curl -s "https://$DOMAIN/.api/functions/graphql" \
  -H "Content-Type: application/json" \
  -d '{"query":"mutation { createComment(input: {name: \"Ada\", body: \"Fast shipping\"}) { name body } }"}' \
  | jq -c .data
curl -s "https://$DOMAIN/.api/functions/graphql" \
  -H "Content-Type: application/json" \
  -d '{"query":"{ comments { name body } }"}' \
  | jq -c '.data.comments | last'
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
<title>Storefront</title>
{"createComment":{"name":"Ada","body":"Fast shipping"}}
{"name":"Ada","body":"Fast shipping"}
```

CLI: `sevalla apps domains list --app-id "$APP_ID"`. MCP: `get_application`, in `domains`.

### Build from the Dockerfile with the API

After you push the `Dockerfile` and `.dockerignore` from [Deploy with a Dockerfile](#deploy-with-a-dockerfile), [Update application](https://api-docs.sevalla.com/v3/applications/update-application) (`updateApplicationV3`) switches the build strategy. Then repeat the **Deploy**, **Wait for the deployment** and **Check the application** steps:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s -X PATCH "https://api.sevalla.com/v3/applications/$APP_ID" \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"build_type":"dockerfile","dockerfile_path":"Dockerfile","docker_context":"."}' \
  | jq -c '{build_type, dockerfile_path, docker_context}'
```

```json theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
{"build_type":"dockerfile","dockerfile_path":"Dockerfile","docker_context":"."}
```

CLI: `sevalla apps update "$APP_ID" --data '{"build_type":"dockerfile","dockerfile_path":"Dockerfile","docker_context":"."}'`. MCP: `sevalla_api_update` with `updateApplicationV3` and the same body.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.