---
title: "Deploy a Node.js API and PostgreSQL over MCP with while.sh"
url: https://while.sh/en/guides/deploy-node-api-postgresql/
author: "Aloïs Oddon, co-founder of Nextmap"
updated: 2026-11-02
---

# Deploy a Node.js API and its PostgreSQL database from your AI agent, over MCP

Your AI agent already knows how to write your Node.js API. Connected to while over MCP, it deploys it too, with its PostgreSQL database backed up every day, and you never touch a server.

## Prerequisites

- A Node.js API.
- An AI agent that works in a terminal: Claude Code, Codex, Cursor or Gemini CLI.

## Step 1: Give your agent these instructions

Open your agent in your API's folder, and paste these instructions into it:

```text titre="Instructions"
Connect to while with my account (while.sh/agents), then deploy my app.
```

Without an account, your agent deploys your API as a trial, in one request. To keep it, open the link it gives you and create your account.

With your account, your agent adds while's MCP server itself. If it asks you to, restart it. On the first call to while, a page opens in your browser. In Claude Code, if it doesn't open, type /mcp, choose while, then Authenticate. On that page, choose your app, or "A new app", then click Authorize.

## Step 2: Open your API

Your agent gives you your API's address, for example `https://mon-api.while.cloud`.

## What your agent does

It reads the rules for an app on while, like listening on `PORT` and answering 200 on `/health`, then writes while.yaml at the root of your API:

```yaml titre="while.yaml"
apiVersion: while/v1
project: mon-api
services:
  - name: web
    port: 3000
databases:
  - name: main
```

The last two lines add a PostgreSQL database. Its address, password included, reaches your API in `DATABASE_URL`. node-postgres reads it as is (source 1), as in this Express API tested on while:

```js titre="server.js"
import express from 'express';
import pg from 'pg';

const db = new pg.Pool({ connectionString: process.env.DATABASE_URL });
await db.query('CREATE TABLE IF NOT EXISTS visites (id serial PRIMARY KEY, cree_le timestamptz DEFAULT now())');

const app = express();
app.get('/health', (_req, res) => res.send('ok'));
app.get('/', async (_req, res) => {
  await db.query('INSERT INTO visites DEFAULT VALUES');
  const { rows } = await db.query('SELECT count(*)::int AS n FROM visites');
  res.send(`visites : ${rows[0].n}`);
});
app.listen(process.env.PORT || 3000, '0.0.0.0');
```

It then sends the code, which is built, tested, then deployed.

## Choose the PostgreSQL version

> You, to your agent: Create the database on PostgreSQL 18.

You choose the version when the database is created: PostgreSQL 17 by default, 16 or 18 with `version` in while.yaml:

```yaml titre="while.yaml"
databases:
  - name: main
    version: "18"
```

## Plan the disk space

> You, to your agent: Create the database with 10 GiB of disk.

You choose the disk when the database is created, with `size`: 1 GiB by default. Ask for it before the first deployment.

```yaml titre="while.yaml"
databases:
  - name: main
    size: 10Gi
```

## Use Prisma with PostgreSQL

> You, to your agent: Use Prisma to create and migrate the tables.

Your agent writes the schema and runs the migration when the API starts. Prisma reads the database address from `DATABASE_URL`:

```prisma titre="prisma/schema.prisma"
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}
```

```json titre="package.json (excerpt)"
"type": "module",
"scripts": {
  "build": "prisma generate",
  "start": "prisma migrate deploy && node server.js"
},
"dependencies": {
  "@prisma/client": "^6.1.0",
  "prisma": "^6.1.0"
}
```

Prisma goes in `dependencies`: the deployed API is installed without its `devDependencies`. There's no need to add `?pgbouncer=true` to the address: PgBouncer handles prepared statements.

Your agent doesn't declare a `directUrl`: the database's direct address is only given to jobs, and Prisma would stop at startup with error P1012.

## Query PostgreSQL from your agent, over MCP

> You, to your agent: How many visits today?

Your agent reads your database in SQL through while's MCP server, with your permissions on the data, and it can't write to it. Every query is recorded in while's activity log, with its values replaced by $1, $2…

## Export a table to CSV

> You, to your agent: Export the visites table to CSV.

Your agent writes the table to a CSV file, with the same permissions it has to read it.

## Find slow queries

> You, to your agent: Which queries are slowing down the database?

Your agent reads your database's most frequent and slowest queries, as recorded by pg_stat_statements.

## Change the database size

> You, to your agent: Move the database to size M.

Your agent changes `compute` in while.yaml. Before the change, it sees how much more it costs per month. The database then restarts, in about fifteen seconds.

| Size (`compute`) | CPU | Memory | Concurrent clients | Price, excl. VAT |
|---|---|---|---|---|
| S, by default | 0.1 vCPU | 256 MB, up to 1 GB | 200 | €5 per month |
| M | 0.5 vCPU | 1 GB | 400 | €17 per month |
| L | 1 vCPU | 4 GB | 600 | €65 per month |
| XL | 2 vCPU | 8 GB | 800 | €150 per month |

```yaml titre="while.yaml"
databases:
  - name: main
    compute: M
```

## Add a standby instance

> You, to your agent: Add a standby instance to the database.

With `instances: 2`, a second instance is kept up to date continuously, on its own disk, and takes over if the first one goes down. With the Enterprise plan, a production database has two instances by default.

## Back up before a change

> You, to your agent: Take a backup before the migration.

Your agent starts a backup, immediately followed by a test: a throwaway database is created from it, and its tables are read back. Every database is also backed up every day, and its write-ahead log is archived continuously.

## Go back to a backup or a precise time

> You, to your agent: Restore the database to yesterday at 6 pm.

Your database goes back to a backup or to a precise moment, within their retention period: 14 days. In production, your agent gives you a link: the database only goes back once the restore is approved, by you or by an administrator depending on your permissions. A safety backup is taken first, then the restored database replaces the old one.

## Understand a failed deployment

> You, to your agent: Why did the deployment fail?

Your agent receives the error and the last lines of the build log. Your tests run during the build: if they fail, your API keeps its previous version.

For example, `connect ECONNREFUSED 127.0.0.1:5432`: your tests are looking for a database, which isn't connected during the build. Your agent writes them so they pass without it, or turns them off:

```yaml titre="while.yaml"
services:
  - name: web
    port: 3000
    build:
      skip_tests: true
```

## Configure the session behind PgBouncer

`DATABASE_URL` goes through PgBouncer in transaction mode: each transaction can use a different connection. A `SET` outside a transaction only lasts for its query. To configure the session, use `SET LOCAL` inside a transaction:

```js titre="server.js"
const client = await db.connect();
try {
  await client.query('BEGIN');
  await client.query("SET LOCAL statement_timeout = '5s'");
  await client.query('SELECT count(*) FROM visites');
  await client.query('COMMIT');
} catch (e) {
  await client.query('ROLLBACK');
  throw e;
} finally {
  client.release();
}
```

Anything that needs the whole session, like `pg_restore` or a migration that runs `SET` commands, runs in a scheduled job, which your agent can also start on demand. The job receives the database's direct address in `DB_MAIN_DIRECT_URL`.

## Next steps

- [Restrict your API to the people in your company](/en/guides/internal-tool-company-sign-in/)
- [Add a Redis-compatible cache](/en/guides/add-redis-cache/)
- [Compare with Heroku Postgres](/en/comparisons/while-vs-heroku/)

## Sources

1. [node-postgres, connecting with a connection string (connectionString)](https://node-postgres.com/features/connecting), 2026-11-01

## Questions

### How much does a Node.js API with its database cost on while?

With the Free plan, you only pay for resources, excl. VAT: €5 per month for an S database, plus €0.25 per GiB of disk, and for the API, €17 per vCPU and €8.50 per GiB of memory, per month, based on what it uses. An API that uses at most 0.25 vCPU and 0.5 GiB of memory pays at most €8.50 per month, and €13.75 with an S database and its 1 GiB disk.

### Where is my API hosted?

In France, by Nextmap, a French company.

### Does my API go to sleep when nobody calls it?

No, in production it stays awake. With sleep: 30m in while.yaml, it goes to sleep after 30 minutes without a request, and the first request wakes it up in a few seconds.

### Can I put my API on my own domain?

Yes. Your agent declares the domain, you add the two DNS records it gives you, and the HTTPS certificate is issued on the first visit.

### Where do I put my API's keys and passwords?

Never in the code. The key for an external API is entered in while and added to each call: neither your API nor your agent sees it. Other secrets go into while's vault.

### Can I run background processing?

Yes. Ask your agent for a worker, for processing that runs nonstop, or a scheduled job, for work at a set time.

### I'm on Heroku or Render: how do I move over?

Your agent takes over the code, copies the database from a Heroku or Render export (pg_dump), restored by a job, and carries over your environment variables.

### Can I use Express, Fastify or NestJS?

Yes, any Node.js server that starts with a script in its package.json: start, or start:prod for NestJS. Express and NestJS have already been deployed to production on while.

### Can I open my database in pgAdmin or with psql?

No, the database doesn't open from your computer: your agent reads it and exports it for you.

### Do I need to set SSL in the database address?

No, the DATABASE_URL address works as is, without sslmode.

### Do I need to write a Dockerfile?

No. Without a Dockerfile in your repository, while generates one for your API.
