Tanstack Start
Self-hosting

Database

Feeblo needs PostgreSQL. Migrations are bundled into the server image and run automatically on every boot; the db:* scripts cover local development.

Feeblo stores everything in a single PostgreSQL database and uses Drizzle ORM for schema and migrations. This page covers how the server connects, where migrations live, and how to manage the schema locally.

This page is based on packages/db/, packages/db-migrator/, drizzle.config.ts, apps/server/Dockerfile, and the root README.md in .reference/.

One PostgreSQL database

The only variable the database needs is DATABASE_URL, a standard Postgres connection string. It's read as a redacted Effect config in packages/db/src/config.ts (DatabaseConfig):

const url = yield * Config.redacted("DATABASE_URL");

There is no separate host/port/user/database set of env vars — everything comes from the URL. The example points at the dev container:

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

The Drizzle config (packages/db/drizzle.config.ts) confirms Postgres is the only dialect and that the schema is split across two files:

export default defineConfig({
  schema: ["./src/schema/auth.ts", "./src/schema/feedback.ts"],
  out: "./src/migrations",
  dialect: "postgresql",
  dbCredentials: { url: process.env.DATABASE_URL || "" },
});

So the entire Feeblo schema lives in packages/db/src/schema/auth.ts and packages/db/src/schema/feedback.ts.

Connection behaviour at runtime

The server opens the connection through @effect/sql-pg + Drizzle's effect-postgres integration (packages/db/src/database.ts). On startup it runs SELECT 1 as a health check with a jittered retry: up to 10 attempts spaced 1.25 seconds apart, logging a warning each retry and Info once connected. If it can't connect after 10 tries it fails hard (Effect.orDie) and the server won't start.

This means a self-hosted deployment where the database container starts slightly after the server container will still come up — the retries cover the gap. Still, in compose, depends_on: [postgres] is good hygiene.

Migrations run automatically in the server image

The server Dockerfile (apps/server/Dockerfile) bundles the migrations and the migrator, then runs them before the app:

COPY --from=builder /app/packages/db/src/migrations ./migrations
COPY --from=builder /app/packages/db-migrator/dist ./migrate
# ...
CMD ["sh", "-c", "export SERVER_PORT=${SERVER_PORT:-${PORT:-8080}}; \
       node ./migrate/index.js && exec node ./dist/index.js"]

The migrator (packages/db-migrator/src/index.ts) is a tiny script using drizzle-orm/postgres-js/migrator:

  • reads DATABASE_URL; if unset it logs "DATABASE_URL not defined, skipping migrations" and exits 0,
  • otherwise runs all pending migrations from the bundled migrations/ folder,
  • logs how many milliseconds the run took, then exits 0.

No manual migrate step in production

When you run the published ghcr.io/g3root/feeblo-server image, you do not run pnpm db:migrate. Migrations execute on every container boot, idempotently — Drizzle tracks applied migrations in the database, so re-runs are no-ops. If you want to run migrations manually against an external Postgres instead, use pnpm db:migrate from a local checkout (see below) or invoke the migrator entrypoint directly.

Local development scripts

When working from source, the db:* scripts (defined in packages/db/package.json, surfaced at the repo root) all run drizzle-kit / tsx through dotenvx run -f ../../.env, so they read the repo-root .env:

ScriptRunsUse it for
pnpm db:startdocker compose up -dStart the dev Postgres / Mailpit / MinIO stack
pnpm db:migratedrizzle-kit migrateApply pending migrations from packages/db/src/migrations
pnpm db:generatedrizzle-kit generateCreate a new migration file from schema changes
pnpm db:pushdrizzle-kit pushPush the schema straight to the DB without a migration file
pnpm db:studiodrizzle-kit studioOpen Drizzle Studio for browsing the DB
pnpm db:seedtsx seed.tsSeed sample data (nukes first — don't run on shared data)
pnpm db:nuketsx nuke.tsDrop and recreate the database
pnpm db:stop / db:downdocker compose stop / downPause / tear down the dev containers

The db:generate → commit → db:migrate loop is the usual development cycle: edit packages/db/src/schema/*.ts, generate a migration, check the SQL in packages/db/src/migrations, apply it.

What's in the migrations folder

packages/db/src/migrations ships numbered SQL files plus a meta/ directory that Drizzle uses to track state:

packages/db/src/migrations/
  0000_previous_iron_fist.sql
  0001_daily_marvel_boy.sql
  0002_known_misty_knight.sql
  0003_spicy_black_bird.sql
  meta/

The exact names (*_iron_fist, *_misty_knight, …) are Drizzle's auto-generated slugs; the 00000003 ordering is what matters. The server image copies this whole folder in, so the migrator always has the full history.

Pointers

  • Version — Feeblo tests against postgres:17-alpine in the dev compose; Postgres 16/17 are recommended. Older versions may work but aren't exercised.
  • External Postgres — to use a managed Postgres instead of the compose service, set DATABASE_URL to its connection string and drop the postgres service; the server's retry logic handles the connection.
  • Backups — Feeblo doesn't manage these for you. The dev compose uses a named postgres: volume; in production, snapshot the database yourself.

On this page