Brain API
Brain exposes three classes of contracts:
- REST/HTTP — Clerk-authenticated endpoints for fennec/brisket and
@ApiKeyRoute()endpoints for service-to-service. - gRPC — internal generation surface called by sirloin (
generation.grpc.controller.ts). - BullMQ queues — async generation, character refresh, and shop-VI import workflows.
Code paths referenced are absolute under apps/brain/.
Auth model summary
Both ClerkAuthGuard and ApiKeyAuthGuard are registered globally as APP_GUARD
(apps/brain/src/app.module.ts). Per-route metadata flips behavior:
| Decorator | Source | Effect |
|---|---|---|
@Public() | common/decorators/public-route.decorator.ts | Skips both guards. |
@ApiKeyRoute() | common/decorators/api-key-route.decorator.ts | Requires Authorization: Bearer <key> matching AUTHORIZED_KEYS. |
@Roles(...) | common/decorators/roles.decorator.ts | Adds RolesGuard requirement on top of Clerk; default required roles are ROOT, ADMIN. |
Cross-link: Auth Model, Auth Boundaries.
REST endpoints
Routes are grouped by controller. Auth column: Clerk = end-user JWT/OAuth via ClerkAuthGuard; API Key = AUTHORIZED_KEYS. Roles, where present, are enforced by RolesGuard.
Application settings — application-settings
Source: apps/brain/src/modules/application/applicationSettings/controllers/application-settings.http.controller.ts
| Method | Path | Auth | Notes |
|---|---|---|---|
| GET | /application-settings | Clerk | List settings keys. |
| PUT | /application-settings/:id | Clerk + Roles | Body must include data; Zod-validated, throws BadRequestException on shape mismatch. |
Storage — storage
Source: apps/brain/src/modules/application/storage/controllers/storage.http.controller.ts
| Method | Path | Auth | Notes |
|---|---|---|---|
| GET | /storage/signed-url | Clerk | S3/R2 GET URL. |
| GET | /storage/signed-urls | Clerk | Batch GET URLs. |
| GET | /storage/public-url-template | Clerk | Returns BRAIN_PUBLIC_BUCKET_URL template. |
API surface (service-to-service) — api/*
Sources: apps/brain/src/modules/domain/api/controllers/*.http.controller.ts
| Method | Path | Auth | Notes |
|---|---|---|---|
| POST | /api/onboarding/kitsune/image-score | API Key | Score onboarding image. |
| GET | /api/onboarding/test-imgd/:key | API Key | Internal diagnostic. |
| POST | /api/onboarding/test-cropping | API Key | Internal diagnostic. |
| POST | /api/onboarding/reference-nudify | API Key | Nudify 3 SFW reference images; returns nudified paths. See Reference Nudify. |
| POST | /api/webhook/rp/training | API Key | Deprecated — throws NotImplementedException. |
| POST | /api/webhook/vi/created | API Key | VI Generator batch-created webhook; calls ShopVIImportService.handleWebhookBatch. Returns {status: "accepted"}. |
| POST | /api/character/kitsune | API Key | Provision Kitsune character. |
| GET | /api/character/vi/catalogue | API Key | VI catalogue list. |
| GET/POST/PATCH/DELETE | /api/character, /api/character/:id | API Key | Character CRUD used by sirloin. |
| GET/POST/PATCH/DELETE | /api/media, /api/media/:id | API Key | Media CRUD used by sirloin (media.http.controller.ts). |
Character (admin) — character/*, body-type, dataset, character-control-group, dataset-scoring
Sources: apps/brain/src/modules/domain/character/controllers/*.http.controller.ts. All Clerk-authenticated, role-gated by default to ROOT, ADMIN via global RolesGuard.
Body Parts Library — body-parts-library
Source: apps/brain/src/modules/domain/character/controllers/body-parts-library.http.controller.ts. Clerk-authenticated, role-gated.
| Method | Path | Auth | Notes |
|---|---|---|---|
| GET | /body-parts-library | Clerk + Roles | List items, optional body_part filter. |
| POST | /body-parts-library | Clerk + Roles | Upload reference image with body_part + attributes (multipart). |
| DELETE | /body-parts-library/:id | Clerk + Roles | Remove item and its stored image. |
Stores reference body-part images (bust, genitalia) with attribute metadata used by the nudify endpoint. Managed via the Fennec Body Parts Library page.
Reference Nudify — api/onboarding/reference-nudify
Optional character creation step that enables NSFW capabilities for users who choose not to upload real nude images. Instead of requiring actual NSFW photos, the system takes the user’s SFW onboarding images and generates synthetic NSFW reference variants using AI.
Accepts 3 SFW onboarding images (FACE_FRONTAL, FULL_BODY, FULL_BODY_ANY) and returns AI-generated nudified versions using WaveSpeed’s flux-2-klein-9b/edit-lora model with externally-hosted LoRA weights.
Request:
items[]— 3 objects withtype(KitsuneOnboardingImageType) andpath(R2 key)bust_size— one ofsmall,medium,big,hugegenitalia_style— one ofshaved,hairy
Behavior:
- Picks a random matching bust and genitalia reference image from the Body Parts Library.
- Reads LoRA configuration from
nudify-me-lora-inputapplication setting. - Resolves output size to maximize dimensions within 256—1536 px per side while preserving aspect ratio.
- Launches 3 concurrent WaveSpeed inference jobs with body-part-specific prompts.
- Polls for completion, downloads results, uploads to
characters/nudify-results/.
Response: Same 3 items with added nudified_path.
Source: apps/brain/src/modules/domain/character/services/nudify.service.ts, apps/brain/src/modules/domain/api/controllers/onboarding.http.controller.ts
Generation (admin) — generation/*, tags, presets, settings, examples, prompt-template, motion-type, input-image
Sources: apps/brain/src/modules/domain/generation/controllers/*.http.controller.ts. Clerk-authenticated. The HTTP layer manages reference data; queue jobs do the heavy lifting.
Dashboard-specific generation endpoints back Fennec’s Generation Dashboard:
| Method | Path | Notes |
|---|---|---|
| GET | /generation/dashboard | Cursor-paginated list of generations created in the rolling last 30 days. Rows are included even when adapter_params or logs are missing. Supports model, status, character, and media-type filters. |
| GET | /generation/dashboard/:id | Single generation detail by genId; no recency cutoff, so older linked generations remain inspectable. Missing adapter params/logs return empty arrays. |
| GET | /generation/dashboard/models | Model URL list used by the dashboard filter. |
The list cutoff is backed by idx_generation_dashboard_created_at_dbid on
Generation(created_at DESC, dbId DESC).
User — user/*
Source: apps/brain/src/modules/domain/user/controllers/user.http.controller.ts. Resolves Brain user by Clerk ID; populates request.currentUser via RolesGuard.
Moderation — moderation/*
Source: apps/brain/src/modules/domain/moderation/controllers/moderation.http.controller.ts.
Spend — spend/*
Source: apps/brain/src/modules/domain/spend/controllers/spend.http.controller.ts.
Shop-VI — virtual-character-import/*
Source: apps/brain/src/modules/domain/shop-vi/controllers/virtual-character-import.http.controller.ts.
Bull Board UI — /queues
Mounted by BullBoardModule.forRoot({ route: '/queues', adapter: ExpressAdapter }) in app.module.ts. Clerk-protected. Read-only inspector for all registered queues (bullBoardQueues.module.ts).
Standard error envelope
All HTTP failures pass through HttpExceptionFilter
(apps/brain/src/common/filters/http-exception.filter.ts). Response shape:
{ "statusCode": 400, "timestamp": "2026-05-05T12:00:00.000Z", "path": "/api/character/abc", "message": "..."}5xx and unexpected errors are auto-reported to Sentry via @sentry/nestjs. See Brain Errors for code-by-code remediation.
gRPC
A single hybrid gRPC server is started in main.ts via app.startAllMicroservices(). Generated stubs live under apps/brain/src/generated/round/v1/round.ts (round client) and proto definitions are owned at the repo root in proto/.
Server-side controller:
| File | Service |
|---|---|
apps/brain/src/modules/domain/generation/controllers/generation.grpc.controller.ts | Generation gRPC handlers consumed by sirloin. |
Client (outbound) usage: apps/brain/src/modules/application/round/services/* calls round over gRPC. See round for the round-side contract.
BullMQ queues
Queue names are defined in apps/brain/src/common/constants.ts:
export enum QUEUES { MEDIA_FLOWS = 'mediaflows', EXAMPLE_EXPLICITNESS = 'exampleexplicitness', SHOP_VI_IMPORT = 'shopviimport', KITSUNE_SFW_IMAGE_FROM_NSFW_GENERATION = 'kitsunesfwimagefromnsfwgeneration',}Connection is configured by apps/brain/src/config/queue.config.ts against REDIS_HOST / REDIS_PORT / REDIS_PASSWORD.
| Queue | Processor | Concurrency | Idempotency | Purpose |
|---|---|---|---|---|
mediaflows | MediaFlowsProcessor | 500 | Job ID = generationRequest.mediaId (uuid); failures recorded via GenerationEventLogService.markError | Drives the generation pipeline (image, image-seq, image-to-video, etc.). Catches ContentModerationException separately. |
exampleexplicitness | ExampleExplicitnessProcessor | 1, batch 500 | Per tagId re-classification | Re-classifies GenerationExample records when a tag’s explicitness changes. |
shopviimport | ShopVIImportProcessor | 1 | Per webhook job_id from VI Generator | Imports characters from a Shop-VI batch into the fennec schema. Triggered by POST /api/webhook/vi/created. |
kitsunesfwimagefromnsfwgeneration | KitsuneSfwImageFromNsfwGenerationProcessor | 20 | Per characterId + nsfwSourceTypes | Generates SFW reference images from NSFW sources for Kitsune characters. |
Default job options (apps/brain/src/config/queue.config.ts): completed jobs retained 24h or last 1,000; failed jobs retained 7 days or last 5,000.
flowchart LR sirloin -- gRPC --> brain brain -- enqueue --> mediaflows brain -- enqueue --> kitsune[kitsunesfwimagefromnsfwgeneration] brain -- enqueue --> shopvi[shopviimport] vigen[VI Generator] -- POST /api/webhook/vi/created --> brain brain -- gRPC --> roundWebhooks
| Direction | Endpoint / Target | Source/Target | Auth | Notes |
|---|---|---|---|---|
| Inbound | POST /api/webhook/vi/created | VI Generator (BRAIN_VI_GENERATOR_URL) | API Key | See above. |
| Inbound | POST /api/webhook/rp/training | RunPod (legacy) | API Key | Deprecated; returns 501. |
| Outbound | RunPod / FAL / Wavespeed / Kling / OpenRouter / Google Vertex | Provider URLs in env | Provider-specific | Driven from queue processors and adapter modules. |
Chargebee webhooks are not handled by brain — they terminate at sirloin (see Sirloin Billing).
Examples
Service-to-service character lookup
curl -H "Authorization: Bearer $BRAIN_API_KEY" \ http://brain:3000/api/character/abc-123VI Generator webhook
curl -X POST -H "Authorization: Bearer $BRAIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"job_id":"...","characters":[...]}' \ http://brain:3000/api/webhook/vi/createdTODO
- TODO(@pawel): Confirm complete list of Clerk-authenticated
character/*andgeneration/*routes against current controller bodies (only headline routes documented here; controllers underapps/brain/src/modules/domain/character/controllers/andapps/brain/src/modules/domain/generation/controllers/). - Swagger UI is mounted at
/api-docswith the JSON document at/api-json(apps/brain/src/main.ts:70-74). - No GraphQL controllers are registered; brain exposes REST (HTTP) and a gRPC microservice (
apps/brain/src/main.ts:55-66).