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
| Tool | Version | Why |
|---|---|---|
| Node.js | 24+ | Engines constraint in apps/brain/package.json; older Node will refuse to install. |
| pnpm | 10.26.0 | Pinned in CI (.github/workflows/brain.yml). |
| Docker + Compose | recent | Postgres (rump) and Redis come from the root docker-compose.yml. |
| make | any | Repo entry points use make — make dev-build, make generate-proto. |
Optional but recommended:
psqlfor poking at the fennec schema.redis-clifor queue inspection without Bull Board.
One-time setup
# Clone and installgit clone git@github.com:dreamworld-research/beef.gitcd 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 generateEnvironment
Copy the repo-root example and fill the BRAIN_* keys you need:
cp .env.example .envMinimum to boot brain locally with the rest of the stack:
BRAIN_STAGE=developmentBRAIN_HTTP_PORT=3000BRAIN_DATABASE_URL=postgresql://sirloin:sirloin@rump:5432/fennec?sslmode=disable&schema=fennecBRAIN_DIRECT_DATABASE_URL=BRAIN_REDIS_HOST=redisBRAIN_REDIS_PORT=6379BRAIN_CLERK_PUBLISHABLE_KEY=...BRAIN_CLERK_SECRET_KEY=...BRAIN_AUTHORIZED_KEYS=secretkey1,secretkey2BRAIN_FRONTEND_URL=http://localhost:9940BRAIN_S3_BUCKET_NAME=tenderloin-devBRAIN_AWS_ACCESS_KEY_ID=...BRAIN_AWS_SECRET_ACCESS_KEY=...BRAIN_AWS_S3_APIREGION=autoBRAIN_AWS_S3_APIURL=https://...r2.cloudflarestorage.comBRAIN_PUBLIC_BUCKET_URL=https://tenderloin.letsfoxy.comBRAIN_ROUND_HOST=round:8080Provider 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
make dev-build # build all imagesmake dev-up-d # detached, including brain (port 9970:3000), rump (Postgres), redis, rounddocker compose logs -f brainBrain 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
docker compose up -d rump redis round # bring up just the depspnpm --filter brain start:dev # nest start --watch on hoststart: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:
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-dThe 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=fennecBRAIN_DIRECT_DATABASE_URL=postgresql://<role>:<password>@<direct-host>/<db>?sslmode=require&schema=fennecWith 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
# Generate client after schema editspnpm --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 pipelineexampleexplicitness— tag re-classificationshopviimport— VI import via webhookkitsunesfwimagefromnsfwgeneration— 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
pnpm --filter brain start:dev # nest start --watch with prisma generatepnpm --filter brain test # unit testspnpm --filter brain lint # ESLint, --max-warnings 0pnpm --filter brain tsc # typecheckSee 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/clientare imported across the codebase. CI does this; locallystart:devdoes it. Barepnpm tscafter a fresh checkout will fail until you runpnpm prisma generate. - ESLint
--max-warnings 0: any warning fails CI. Don’t// eslint-disablecasually. - 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.tsbinds 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' })inmain.ts). Smaller proxies in front may reject before that. - Migrations and pooled connections:
prisma migrate deploymust useDIRECT_DATABASE_URL(a non-pooled connection). With pgbouncer, migrations will hang or fail. - Local Neon migrations:
apps/brain/create_migration.shleaves Neon hosts intact. IfBRAIN_DATABASE_URLis pooled, setBRAIN_DIRECT_DATABASE_URLtoo. - Schema is shared with sirloin: brain owns the
fennecschema inside the same Postgres cluster sirloin uses. Don’t drop the database from brain tooling. - Custom validation pipe:
CustomValidationPipeis global (main.ts). DTOs useclass-validator+class-transformer; missing decorators will allow request-shape drift through the type system.
TODO
apps/brain/package.jsonpins"packageManager": "pnpm@10.27.0"while.github/workflows/brain.ymlpinsPNPM_VERSION: 10.26.0. TODO(@pawel): align CI to thepackageManagerfield.- TODO(@pawel): Document seeding / fixture flow for the fennec schema in development (no
prisma/seed.tsexists today;apps/brain/prisma/contains onlyschema.prismaandmigrations/).