---
title: Database
description: "Postgres for Feeblo. The DATABASE_URL shape, compose defaults, and the migration lifecycle from schema change to deployed schema."
---

Feeblo keeps all of its state in one PostgreSQL database. The compose stack runs
PostgreSQL 17 with the pgvector extension, and the server container applies migrations
itself on every start, so a production database mostly runs itself. The `db:` scripts
matter when you develop locally, manage the schema by hand, or recover a broken instance.

## Connection string

The database URL follows the standard Postgres shape:

```bash
DATABASE_URL="postgres://feeblo:password@127.0.0.1:54323/feeblo"
```

| Part       | Meaning                                            |
| ---------- | -------------------------------------------------- |
| `feeblo`   | user, `POSTGRES_USER` in compose                   |
| `password` | password, `POSTGRES_PASSWORD` in compose           |
| `127.0.0.1`| host:port the server can reach                     |
| `feeblo`   | database name, `POSTGRES_DB` in compose            |

Compose fills in sensible defaults for you:

- The `pg` container reads `POSTGRES_USER`, `POSTGRES_PASSWORD`, and `POSTGRES_DB`
  from `.env`, each defaulting to `feeblo`, `password`, `feeblo`.
- The server gets `DATABASE_URL` defaulting to `postgres://feeblo:password@pg:5432/feeblo`.
  The host is `pg` because that is the service name inside the compose network.
- On the host, Postgres publishes to `${POSTGRES_PORT:-5432}:5432`, so a local client
  connects to `127.0.0.1:5432` by default. The dev stack uses port `54323` instead.

:::warning[Data lives in the postgres volume]
The `pg` service stores everything under `/var/lib/postgresql/data` in a named volume
called `postgres`. Recreating containers is safe; deleting the volume deletes your data.
Back up with `docker compose exec pg pg_dump -U feeblo feeblo > feeblo-backup.sql`
before upgrades or volume surgery.
:::

## Migration lifecycle

Schema lives in `packages/db/src/schema/` (four files: `auth.ts`, `feedback.ts`,
`integration.ts`, `media.ts`). Drizzle generates versioned SQL migrations from them into
`packages/db/src/migrations/`.

1. **Change the schema**

    Edit the Drizzle schema in `packages/db/src/schema/`. Add tables, columns, or indexes
    the same way you would in any Drizzle project.

2. **Generate the migration**

    ```bash
    pnpm db:generate
    ```

    `drizzle-kit generate` diffs the schema against the last migration and writes a new
    timestamped folder under `packages/db/src/migrations/`. Review the SQL it produced.

3. **Commit the migration**

    Migrations are code. Commit the generated folder with the schema change so the next
    deploy carries it.

4. **Apply it locally or deploy it**

    ```bash
    pnpm db:migrate
    ```

    `drizzle-kit migrate` applies every pending migration to the `DATABASE_URL` found in the
    root `.env` file and records it in the `__drizzle_migrations` table.

    Production never runs this command. The server image runs its own migration runner
    (`node ./migrate/index.js`) before it starts listening, so a deploy applies pending
    migrations automatically. See [Deploy with Docker](/self-hosting/docker) for the upgrade
    flow.

## The db: scripts

All scripts run from the repo root and load `.env` through `dotenvx`, so keep
`DATABASE_URL` in your root `.env` before running any of them.

| Script               | What it does                                                           |
| -------------------- | ---------------------------------------------------------------------- |
| `pnpm db:start`      | Start the local database container (`docker compose up -d`)            |
| `pnpm db:watch`      | Same, with logs attached (`docker compose up`)                         |
| `pnpm db:generate`   | Generate SQL migrations from schema changes                            |
| `pnpm db:migrate`    | Apply pending migrations                                               |
| `pnpm db:push`       | Push the schema without a migration file (`drizzle-kit push`)          |
| `pnpm db:seed`       | Fill the database with sample data                                     |
| `pnpm db:studio`     | Open Drizzle Studio against the database                               |
| `pnpm db:nuke`       | Erase every row in every table                                         |
| `pnpm db:stop`       | Stop the database container                                            |
| `pnpm db:down`       | Stop and remove the container (data volume survives)                   |

:::warning[db:nuke destroys data]
`pnpm db:nuke` runs `packages/db/src/nuke.ts`, which truncates every table in the
`public` schema with `RESTART IDENTITY CASCADE`. Only the `__drizzle_migrations` table
survives, so migration history stays intact but every row goes away. There is no undo.
Never run it against a database you care about.
:::

`pnpm db:push` is the fast path for throwaway development databases: it applies the
schema directly instead of going through migrations. On a shared or production database,
prefer generated migrations, because `push` leaves no migration record for other
environments to replay.

## Local development stack

The dev compose file at `docker/docker-compose.dev.yml` defines Postgres, Redis,
MinIO, and Mailpit:

```bash
docker compose -f docker/docker-compose.dev.yml up -d
```

Postgres publishes on port `54323`, which is exactly what the `DATABASE_URL` default in
`.env.example` points at (`postgres://feeblo:password@127.0.0.1:54323/feeblo`). A fresh
local checkout needs three commands:

```bash
pnpm db:migrate
pnpm db:seed
pnpm dev
```

`pnpm db:seed` creates an admin user and sample data so the dashboard has something to
show. Re-run `db:migrate` whenever you pull changes that contain new migrations.

If you change embedding dimensions, run `pnpm db:configure-embeddings` to resize the
vector column; the [Docker page](/self-hosting/docker) explains when that is needed.

## Next steps

<CardGroup cols={2}>
  <Card title="Environment variables" href="/self-hosting/environment">
    Every variable Feeblo reads, grouped and marked required or optional.
  </Card>
  <Card title="Deploy with Docker" href="/self-hosting/docker">
    The compose walkthrough, first boot, and upgrade path.
  </Card>
  <Card title="Email" href="/self-hosting/email">
    SMTP transports, provider webhooks, outbox controls.
  </Card>
  <Card title="Media" href="/self-hosting/media">
    S3-compatible object storage for uploads.
  </Card>
</CardGroup>