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
Section titled “Prerequisites”| Tool | Version | Why |
|---|---|---|
| Node.js | 24+ | Engines constraint in apps/brain/package.json; older Node will refuse to install. |
| pnpm | 10.34.5 | Pinned in apps/brain/package.json and CI. |
| 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
Section titled “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
Section titled “Environment”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
Section titled “Running brain”Option A — full stack via docker-compose
Section titled “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
Section titled “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/package.json).
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
Section titled “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.
Observability — local OTel mockup
Section titled “Observability — local OTel mockup”The OTel SDK is gated to production/staging (isBrainOtelStageEnabled in
apps/brain/src/common/otel/runtime-config.ts), so it stays off under the
default local BRAIN_STAGE=sandbox. To exercise the pipeline locally without
impersonating staging, which would disable the roles.guard.ts dev-admin
auto-bypass for sandbox/development, set:
BRAIN_OTEL_LOCAL_MOCK=1This does two things in runtime-config.ts:
- Adds a bypass to
isBrainOtelAllowedso the gate passes regardless of stage. - Self-defaults
BRAIN_OTEL_URLtohttp://127.0.0.1:4318(override the port withBRAIN_OTEL_LOCAL_MOCK_PORT— validated to an integer in1..65535, otherwise falling back to4318) and both header vars tox-otel-local-mock=1when they aren’t already set, so the flag alone is enough — no manualBRAIN_OTEL_URL/header configuration needed.
otel.server.config.ts then starts an in-process collector
(apps/brain/src/common/otel/local-mock-collector.ts) on that port before
building the exporters. BRAIN_OTEL_LOCAL_MOCK only supplies a fallback
default — it never overrides an explicit BRAIN_OTEL_URL. As long as
BRAIN_OTEL_URL is left unset, traces/logs export to itself inside the same
container and nothing leaves the machine; if BRAIN_OTEL_URL is set to a real
endpoint, exports go there instead. The collector binds 127.0.0.1 only —
unreachable from other containers on the compose network — and caps request
bodies at 10 MB while streaming, replying 413 and dropping the connection
before Buffer.concat on anything larger. It logs a header line per received
export ([brain] otel-local-mock POST /v1/...), and for recognized trace/log
payloads (both export as JSON in this OTel JS version) a decoded,
pretty-summarized detail line per span or log record — up to 25 per export,
then ... [N more] — before discarding everything. The metrics exporter (@opentelemetry/exporter-metrics-otlp-proto)
sends protobuf instead of JSON; the mock doesn’t decode it, so those requests
are logged as <binary ..., N bytes> only. It replies with an empty body for
application/x-protobuf requests specifically — replying with {} there trips
the metrics exporter’s response deserializer with Unknown wire type.
Deliberately absent from .env.example — local-only, never set outside a
checkout.
Database — Prisma
Section titled “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/AGENTS.md before deviating.
Queues — BullMQ + Redis
Section titled “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
Section titled “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
Section titled “Common gotchas”- Node 24.x:
apps/brain/package.jsonrequires>=24.0.0 <25.0.0. Usenvm/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(@pawel): Document seeding / fixture flow for the fennec schema in development (no
prisma/seed.tsexists today;apps/brain/prisma/contains onlyschema.prismaandmigrations/).