Skip to content

Brain API

Brain uses three classes of contracts:

  1. REST/HTTP — Clerk-authenticated endpoints for fennec/brisket and @ApiKeyRoute() endpoints for service-to-service.
  2. Outbound gRPC — Brain calls Round for model-serving work; it does not currently expose an inbound gRPC server.
  3. BullMQ queues — async generation, character refresh, and shop-VI import workflows.

Code paths referenced are absolute under apps/brain/.

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.

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

Section titled “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.

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/*

Section titled “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

Section titled “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.

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). Rejects values outside the taxonomy with 400.
DELETE /body-parts-library/:id Clerk + Roles Remove item and its stored image.

Stores reference body-part images tagged with a body-part slot and filter attributes. Slots, filters and their allowed values come from the body-parts-library-taxonomy application setting, and the upload rejects any slot, filter key or value the taxonomy does not declare. Read by the character:pick_stock_body_reference workflow node and by the legacy nudify endpoint, which do not share matching semantics. Managed via the Fennec Body Parts Library page. See Body Parts Library for the taxonomy, the wildcard matching rule and both consumers.

Path: 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 with type (KitsuneOnboardingImageType) and path (R2 key)
  • bust_size – one of small, medium, big, huge
  • genitalia_style – one of shaved, hairy

Behavior:

  1. Picks a random matching bust and genitalia reference image from the Body Parts Library.
  2. Reads LoRA configuration from nudify-me-lora-input application setting.
  3. Resolves output size to maximize dimensions within 256–1536 px per side while preserving aspect ratio.
  4. Launches 3 concurrent WaveSpeed inference jobs with body-part-specific prompts.
  5. 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

Section titled “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).

Source: apps/brain/src/modules/domain/user/controllers/user.http.controller.ts. Resolves Brain user by Clerk ID; populates request.currentUser via RolesGuard.

Source: apps/brain/src/modules/domain/moderation/controllers/moderation.http.controller.ts.

Source: apps/brain/src/modules/domain/spend/controllers/spend.http.controller.ts.

Source: apps/brain/src/modules/domain/shop-vi/controllers/virtual-character-import.http.controller.ts.

Mounted by BullBoardModule.forRoot({ route: '/queues', adapter: ExpressAdapter }) in app.module.ts. Clerk-protected. Read-only inspector for all registered queues (bullBoardQueues.module.ts).

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 logged at error severity through Pino and exported to Axiom when log export is configured. See Brain Errors for code-by-code remediation.

Brain does not currently start an inbound gRPC server; the former bootstrap block in main.ts is commented out. Generated Round client stubs live under apps/brain/src/generated/round/v1/round.ts, and apps/brain/src/modules/application/round/services/* calls Round over gRPC. Sirloin calls Brain over the generated HTTP/OpenAPI client.

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 -- HTTP --> brain
  brain -- enqueue --> mediaflows
  brain -- enqueue --> kitsune[kitsunesfwimagefromnsfwgeneration]
  brain -- enqueue --> shopvi[shopviimport]
  vigen[VI Generator] -- POST /api/webhook/vi/created --> brain
  brain -- gRPC --> round
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).

Terminal window
curl -H "Authorization: Bearer $BRAIN_API_KEY" \
http://brain:3000/api/character/abc-123
Terminal window
curl -X POST -H "Authorization: Bearer $BRAIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"job_id":"...","characters":[...]}' \
http://brain:3000/api/webhook/vi/created
  • TODO(@pawel): Confirm complete list of Clerk-authenticated character/* and generation/* routes against current controller bodies (only headline routes documented here; controllers under apps/brain/src/modules/domain/character/controllers/ and apps/brain/src/modules/domain/generation/controllers/).
  • Swagger UI is mounted at /api-docs with the JSON document at /api-json (apps/brain/src/main.ts:70-74).
  • No GraphQL controllers or inbound gRPC microservice are registered; Brain exposes HTTP and acts as a gRPC client to Round (apps/brain/src/main.ts:55-74).