---
title: "How to add a PostgreSQL or Redis database to a coding challenge"
description: "List PostgreSQL 18 or Valkey 9 under databases in kendor.yaml. They run inside the sandbox, and code connects with DATABASE_URL or REDIS_URL."
updated: 2026-10-06
canonical: https://kendor.io/docs/environments/databases
---

# How to add a PostgreSQL or Redis database to a coding challenge
To give a challenge a database, list it under `databases` in `kendor.yaml`. Kendor runs PostgreSQL 18
and Valkey 9 (Redis compatible) inside the candidate's sandbox on `127.0.0.1`, starts them before any
install command, and sets `DATABASE_URL` and `REDIS_URL` for every process, so the code connects
without any setup from the candidate.

## Declare a database

```yaml file="kendor.yaml"
version: 1
runtime:
  type: node
  version: "22"
databases:
  - type: postgres
    version: "18"
    init: db/seed.sql
  - type: valkey
    version: "9"
services:
  app:
    install:
      commands:
        - npm ci
    dev:
      command: node --watch src/server.js
      port: 3000
    test:
      command: node --test
```

| Key | Required | Description |
|---|---|---|
| `type` | Yes | `postgres` or `valkey`. Each at most once. |
| `version` | Yes | `"18"` for PostgreSQL, `"9"` for Valkey, as a quoted string. |
| `name` | No | PostgreSQL only. The database to create. Default `app`. Lowercase letters, digits and underscores, starting with a letter or underscore, and not `postgres`, `template0` or `template1`. |
| `init` | No | PostgreSQL only. A `.sql` file, or a folder of them, relative to the workspace root. |

You can also add databases in the **Runtime configuration** dialog under **Databases**, with an
optional **Seed file**. The **Express + PostgreSQL + Valkey** starter for Node.js is a complete
working example.

## Seed data with init

`init` runs once, right after the database is created:

- A file runs on its own. A folder runs every `.sql` file in it in name order, so prefix files
  with numbers: `01_schema.sql`, `02_data.sql`.
- Everything runs in one transaction as the app's own user. If any statement fails, nothing is
  applied.
- If the seed fails while the sandbox is running, the database keeps running and the error shows in
  its process. Fix the file, then restart the **Database** process from the **Processes** view to
  run it again.

Migrations that your framework runs (`prisma migrate deploy`, `php artisan migrate`) can go in an
`install` command instead: databases are already up when install commands start.

## Connection details

Both servers listen on `127.0.0.1` only. PostgreSQL connects as the user `kendor` with no password,
which owns the database but isn't a superuser.

### PostgreSQL

| Variable | Value |
|---|---|
| `DATABASE_URL` | `postgresql://kendor@127.0.0.1:5432/app` (with your `name` in place of `app`) |
| `PGHOST`, `PGPORT`, `PGUSER`, `PGDATABASE` | `127.0.0.1`, `5432`, `kendor`, the database name |
| `DB_HOST`, `DB_PORT`, `DB_DATABASE`, `DB_USERNAME`, `DB_PASSWORD` | The same values, for Laravel. `DB_PASSWORD` is empty. |
| `SPRING_DATASOURCE_URL`, `SPRING_DATASOURCE_USERNAME`, `SPRING_DATASOURCE_PASSWORD` | `jdbc:postgresql://127.0.0.1:5432/app`, `kendor` and empty, for Spring Boot. |

Because these are real environment variables, they take precedence over a Laravel `.env` file or a
Spring `application.properties`.

### Valkey

| Variable | Value |
|---|---|
| `REDIS_URL`, `VALKEY_URL` | `redis://127.0.0.1:6379` |
| `REDIS_HOST`, `REDIS_PORT` | `127.0.0.1`, `6379` |
| `SPRING_DATA_REDIS_HOST`, `SPRING_DATA_REDIS_PORT` | The same, for Spring Boot. |

Valkey speaks the Redis protocol, so any Redis client works.

If your code reads a different variable name, map it in the service's `dev.env.vars` with
`databases.postgres.url` or `databases.valkey.url`:

```yaml file="kendor.yaml"
version: 1
runtime:
  type: python
  version: "3.12"
databases:
  - type: postgres
    version: "18"
    name: inventory
services:
  app:
    install:
      commands:
        - pip install --user -r requirements.txt
    dev:
      command: uvicorn main:app --host $HOST --port $PORT --reload
      port: 8000
      env:
        vars:
          SQLALCHEMY_DATABASE_URI: databases.postgres.url
```

Candidates see the same details in the editor: the **Processes** view has a **Databases** section
with each connection URL and every variable set for it. `psql` and `valkey-cli` work in the terminal
without arguments.

## Connect from code

Read the URL from the environment. Add the client library to your project's dependencies (`pg` and
`redis` for Node.js, `psycopg` and `redis` for Python) so the install command fetches it.

```js title="Node.js" file="src/db.js"
import pg from 'pg';
import { createClient } from 'redis';

export const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });

export const cache = createClient({ url: process.env.REDIS_URL });
await cache.connect();

const { rows } = await pool.query('SELECT id, title FROM todos ORDER BY id');
await cache.set('todos', JSON.stringify(rows));
```

```python title="Python" file="db.py"
import json
import os

import psycopg
import redis

conn = psycopg.connect(os.environ["DATABASE_URL"])
cache = redis.Redis.from_url(os.environ["REDIS_URL"])

with conn.cursor() as cur:
    cur.execute("SELECT id, title FROM todos ORDER BY id")
    rows = cur.fetchall()

cache.set("todos", json.dumps(rows))
```

## What happens to the data

During a session, data persists. Rows are stored in the sandbox's own cache folder, so they survive
a restart of the database process or of the whole sandbox, and the `init` seed doesn't run a second
time.

The **Tests** tab runs against that same live database. Write tests that create the rows they need
and clean up after themselves, or wrap each test in a transaction, so a candidate's experiments in
the terminal can't change the result.

## Limits

| | Limit |
|---|---|
| PostgreSQL connections | 20 |
| PostgreSQL temporary files | 1 GB per connection |
| Valkey memory | 64 MB |
| Data on disk | 1 GB; past that the database servers stop, and the **Database** process says how to start over with an empty database |

The database ports, 5432 and 6379, are never exposed through the preview, even if a server listens
on them.
