Skip to content

Brain Local Development

This page is the canonical local-dev setup for brain. The service is also bootable as part of the full docker-compose stack, but the loop most engineers run is pnpm --filter brain start:dev against a containerised Postgres + Redis.

Prerequisites

ToolVersionWhy
Node.js24+Engines constraint in apps/brain/package.json; older Node will refuse to install.
pnpm10.26.0Pinned in CI (.github/workflows/brain.yml).
Docker + ComposerecentPostgres (rump) and Redis come from the root docker-compose.yml.
makeanyRepo entry points use makemake dev-build, make generate-proto.

Optional but recommended:

  • psql for poking at the fennec schema.
  • redis-cli for queue inspection without Bull Board.

One-time setup

Terminal window
# Clone and install
git clone git@github.com:dreamworld-research/beef.git
cd beef
# Install workspace deps (pnpm workspaces)
pnpm install
# Generate proto stubs (round client lives at apps/brain/src/generated/)
make generate-proto
# Generate Prisma client (also generated by start:dev, but useful early)
pnpm --filter brain prisma generate

Environment

Copy the repo-root example and fill the BRAIN_* keys you need:

Terminal window
cp .env.example .env

Minimum to boot brain locally with the rest of the stack:

BRAIN_STAGE=development
BRAIN_HTTP_PORT=3000
BRAIN_DATABASE_URL=postgresql://sirloin:sirloin@rump:5432/fennec?sslmode=disable&schema=fennec
BRAIN_DIRECT_DATABASE_URL=
BRAIN_REDIS_HOST=redis
BRAIN_REDIS_PORT=6379
BRAIN_CLERK_PUBLISHABLE_KEY=...
BRAIN_CLERK_SECRET_KEY=...
BRAIN_AUTHORIZED_KEYS=secretkey1,secretkey2
BRAIN_FRONTEND_URL=http://localhost:9940
BRAIN_S3_BUCKET_NAME=tenderloin-dev
BRAIN_AWS_ACCESS_KEY_ID=...
BRAIN_AWS_SECRET_ACCESS_KEY=...
BRAIN_AWS_S3_APIREGION=auto
BRAIN_AWS_S3_APIURL=https://...r2.cloudflarestorage.com
BRAIN_PUBLIC_BUCKET_URL=https://tenderloin.letsfoxy.com
BRAIN_ROUND_HOST=round:8080

Provider keys (BRAIN_FAL_AI_KEY, BRAIN_RUNPOD_TOKEN, etc.) are only needed for flows that actually call the provider. See Brain Env for the full list.

docker-compose.yml re-maps BRAIN_* onto the unprefixed names brain actually reads. BRAIN_DIRECT_DATABASE_URL maps to Prisma’s DIRECT_DATABASE_URL; when it is empty, compose falls back to BRAIN_DATABASE_URL.

If you run brain outside compose, export the unprefixed names yourself (e.g. DATABASE_URL, DIRECT_DATABASE_URL, REDIS_HOST, CLERK_SECRET_KEY, AUTHORIZED_KEYS).

Running brain

Option A — full stack via docker-compose

Terminal window
make dev-build # build all images
make dev-up-d # detached, including brain (port 9970:3000), rump (Postgres), redis, round
docker compose logs -f brain

Brain is reachable at http://localhost:9970. Bull Board UI is at http://localhost:9970/queues (Clerk-authenticated).

Option B — brain on host, deps in compose

Terminal window
docker compose up -d rump redis round # bring up just the deps
pnpm --filter brain start:dev # nest start --watch on host

start:dev runs prisma generate automatically before nest watch (see apps/brain/CLAUDE.md Gotchas).

When running on the host, point env at compose-exposed ports (e.g. DATABASE_URL=postgresql://sirloin:sirloin@localhost:8800/fennec?sslmode=disable&schema=fennec).

Option C - brain against Neon

Use this when you need prod-like Postgres behavior without the local rump container. Keep the runtime URL and direct migration URL separate if you use a Neon pooled endpoint:

Terminal window
neonctl auth
NEON_PROJECT_ID='<project-id>' NEON_CREATE_BRANCH=1 make neon-local-env
# Creates local/<git user.email> from staging by default.
# Later runs can omit NEON_CREATE_BRANCH and reuse local/<git user.email>.
make dev-up-neon-d

The helper uses the authenticated neonctl CLI session and writes .env.neon.local with the compose-facing BRAIN_DATABASE_URL and BRAIN_DIRECT_DATABASE_URL values. If NEON_BRANCH is omitted, it uses a stable branch derived from git config user.email; new branches fork from staging by default. To run brain on the host instead of compose, export those values as unprefixed DATABASE_URL and DIRECT_DATABASE_URL. To run only brain and its non-Postgres dependencies in compose, pass NEON_COMPOSE_SERVICES="brain redis round".

BRAIN_DATABASE_URL=postgresql://<role>:<password>@<pooled-host>/<db>?sslmode=require&schema=fennec
BRAIN_DIRECT_DATABASE_URL=postgresql://<role>:<password>@<direct-host>/<db>?sslmode=require&schema=fennec

With those values in .env, make dev-up-d passes them through to DATABASE_URL and DIRECT_DATABASE_URL. For host-run brain, export the same values as unprefixed DATABASE_URL and DIRECT_DATABASE_URL.

Database — Prisma

Terminal window
# Generate client after schema edits
pnpm --filter brain prisma generate
# Apply existing migrations (against DIRECT_DATABASE_URL)
pnpm --filter brain prisma migrate deploy
# Create a new migration — use the project's wrapper, NOT bare `prisma migrate dev`
./apps/brain/create_migration.sh <migration_name>
# or, from apps/brain:
make create-migration NAME=<migration_name>

The custom wrapper starts local rump only when the configured URL uses the compose host rump. Neon URLs are left untouched and use BRAIN_DIRECT_DATABASE_URL / DIRECT_DATABASE_URL for Prisma’s direct migration connection. See apps/brain/CLAUDE.md Gotchas before deviating.

Queues — BullMQ + Redis

Redis is required: brain registers Bull queues at module-load time. Without Redis the app will crash on boot.

Queue names (apps/brain/src/common/constants.ts):

  • mediaflows — generation pipeline
  • exampleexplicitness — tag re-classification
  • shopviimport — VI import via webhook
  • kitsunesfwimagefromnsfwgeneration — Kitsune SFW companion images

Inspect them at http://localhost:9970/queues (Bull Board). Workers are co-located with the API process — there is no separate worker entrypoint.

Useful commands

Terminal window
pnpm --filter brain start:dev # nest start --watch with prisma generate
pnpm --filter brain test # unit tests
pnpm --filter brain lint # ESLint, --max-warnings 0
pnpm --filter brain tsc # typecheck

See Testing Strategy for the brain-specific pyramid (unit-heavy with NestJS testing modules).

Common gotchas

  • Node 24+: lower versions silently produce confusing install errors. Use nvm / volta.
  • Always generate Prisma before lint/tsc: types in node_modules/.prisma/client are imported across the codebase. CI does this; locally start:dev does it. Bare pnpm tsc after a fresh checkout will fail until you run pnpm prisma generate.
  • ESLint --max-warnings 0: any warning fails CI. Don’t // eslint-disable casually.
  • Knip: dead-code detection runs in CI for brain (knip.json). Adding a new exported symbol that nothing imports will fail builds.
  • IPv6 binding: main.ts binds to ::. On macOS this is normally fine; in unusual networking setups (e.g. WSL with IPv6 disabled) you may have to override.
  • Body limit: brain accepts up to 20 MB JSON (bodyParser.json({ limit: '20mb' }) in main.ts). Smaller proxies in front may reject before that.
  • Migrations and pooled connections: prisma migrate deploy must use DIRECT_DATABASE_URL (a non-pooled connection). With pgbouncer, migrations will hang or fail.
  • Local Neon migrations: apps/brain/create_migration.sh leaves Neon hosts intact. If BRAIN_DATABASE_URL is pooled, set BRAIN_DIRECT_DATABASE_URL too.
  • Schema is shared with sirloin: brain owns the fennec schema inside the same Postgres cluster sirloin uses. Don’t drop the database from brain tooling.
  • Custom validation pipe: CustomValidationPipe is global (main.ts). DTOs use class-validator + class-transformer; missing decorators will allow request-shape drift through the type system.

TODO

  • apps/brain/package.json pins "packageManager": "pnpm@10.27.0" while .github/workflows/brain.yml pins PNPM_VERSION: 10.26.0. TODO(@pawel): align CI to the packageManager field.
  • TODO(@pawel): Document seeding / fixture flow for the fennec schema in development (no prisma/seed.ts exists today; apps/brain/prisma/ contains only schema.prisma and migrations/).