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 exits0, - 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:
| Script | Runs | Use it for |
|---|---|---|
pnpm db:start | docker compose up -d | Start the dev Postgres / Mailpit / MinIO stack |
pnpm db:migrate | drizzle-kit migrate | Apply pending migrations from packages/db/src/migrations |
pnpm db:generate | drizzle-kit generate | Create a new migration file from schema changes |
pnpm db:push | drizzle-kit push | Push the schema straight to the DB without a migration file |
pnpm db:studio | drizzle-kit studio | Open Drizzle Studio for browsing the DB |
pnpm db:seed | tsx seed.ts | Seed sample data (nukes first — don't run on shared data) |
pnpm db:nuke | tsx nuke.ts | Drop and recreate the database |
pnpm db:stop / db:down | docker compose stop / down | Pause / 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 0000–0003 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-alpinein 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_URLto its connection string and drop thepostgresservice; 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.
Environment variables
Every environment variable Feeblo understands — what each one does, which app reads it, and the defaults — grouped by section, sourced from .reference/.env.example and the Effect configs.
Media storage
Feeblo uploads profile images, organization logos, and editor media to an S3-compatible bucket. Configure it with MEDIA_UPLOAD_* and optional MEDIA_PUBLIC_BASE_URL.