Skip to content

Brain

Brain

Responsibility

Brain is the NestJS service for generation records, character metadata, provider integrations, and Prisma-owned data in the fennec schema. It also provides reference image nudification (synthetic NSFW variant generation from SFW images) and a body-parts reference library used during character creation.

Under T-Bone, brain owns the workflow engine — the content framework that replaces hand-coded GenerationType branches: workflows, subworkflows, nodes, executions, contracts, and the per-provider inference adapters all live here and are served over an HTTP API (see Workflow engine below).

Runtime

Brain runs as a NestJS HTTP service. Sirloin consumes its OpenAPI-generated HTTP client, Brain calls Round over gRPC for model-serving work, and completed workflows can notify Sirloin over HTTP.

Primary Source Paths

  • apps/brain/src/modules/
  • apps/brain/src/modules/domain/workflow/ (T-Bone workflow engine: engine, nodes, contracts, executions)
  • apps/brain/src/generated/
  • apps/brain/prisma/schema.prisma

Contracts And Generated References

  • Brain HTTP bootstrap and OpenAPI setup live in apps/brain/src/main.ts.
  • Round generated client code lives under apps/brain/src/generated/round/.
  • Sirloin consumes Brain through apps/sirloin/pkg/brain-client/.

Workflow engine

The T-Bone workflow engine lives under apps/brain/src/modules/domain/workflow/. It is the content framework that turns one-off, hand-coded generation types into framework-driven workflows. Core vocabulary:

  • Workflow — a named, versioned graph (stored as JSON) describing end-to-end how one piece of media is generated, from raw inputs through provider inference to a stored result. Declares an input contract and an output contract, carries embedded pricing rules, and normally maps media execution to one Generation row. Sandbox execution creates no Generation unless explicitly requested and applicable. Workflows are drafts until published, which freezes a numbered version. Identified by name + version. See Lifecycle and versioning.
  • Subworkflow — a reusable graph fragment that runs only inside a workflow (one-level nesting, enforced by the save validator). A workflow pins a specific subworkflow version. Each subworkflow node persists its inline run as a child execution linked back to the parent and the node that spawned it. Siblings in a parallel group claim their child concurrently and without a lock: a partial unique index (idx_workflow_execution_one_running_child_per_parent_node) admits at most one RUNNING attempt per parent node, so a losing claimant reuses the winner’s attempt instead of starting a second run. Only a missing or CANCELLED attempt starts fresh; RUNNING and terminal attempts are authoritative and get resumed or replayed.
  • Node — the atomic step: a backend method wrapped by a base class that records start/end events, timing, inputs, outputs, and errors so each step is independently inspectable. Nodes live inside a workflow’s graph and are versioned with it.
  • Engine — the runtime that interprets the graph. It auto-parallelizes independent nodes by topological depth, persists a per-node execution trace (for profiling and crash resume), and enforces cross-cutting behavior an author can’t skip (the moderation wrap on inference nodes). On boot it re-enqueues executions left RUNNING by a crash.
  • Execution — a single run: RUNNING → COMPLETED, FAILED, or CANCELLED, with a per-node trace. The pinned graph is captured at start so a mid-flight edit can’t corrupt an in-progress run. CANCELLED is recorded by the executor on abort-signal propagation (supersession, parent deadline abort into a nested run); whole-run budget expiry fails the execution. HTTP API-triggered cancellation of in-flight executions is out of scope for v1.

Config templates and expressions

A node’s config values reference the workflow’s inputs and upstream node outputs through {{...}} placeholders, resolved at execution time. Two dialects coexist: the original path-and-fallback parser, which is feature-frozen, and the {{= ...}} expression dialect that adds arithmetic and rounding. See Workflow Template Expressions.

Lifecycle and versioning

Status lifecycle: DRAFT → PUBLISHED → SUPERSEDED (replaced by a newer publish) → ARCHIVED (per-name, explicit). Publishing atomically retires the previously published version of the same name to SUPERSEDED, so exactly one version per name is PUBLISHED at any time — enforced by a partial unique index (idx_workflow_one_published_per_name). A superseded version is out of the external-executor path (external executors always resolve the latest published version by name), but it is not dead: subworkflow pins that point at it keep executing it, and it can still be sandbox-run by id (brain logs a warning so testing an outdated version is a deliberate act). Only drafts are deletable; published or superseded versions must be archived first, and archived versions are retained — they keep the name reserved and preserve history.

Contract and surfaces

A workflow’s contract (input_schema, pricing, output_schema) is HTTP-served and is the single source of truth that the UI surfaces render from and that sirloin re-evaluates pricing against:

  • brisket — user-facing forms, dynamic pricing, result presentation.
  • fennec — admin: the same forms plus executing DRAFT workflows, internal-only inputs, and per-node logs.
  • flank — the visual workflow editor, re-pointed to read/write brain over HTTP.
  • Foxy360 / AgentOS — the agent tool surface forwards the caller’s Clerk token to the same Brain endpoints.

Two HTTP surfaces: the “Beef” surface (@Controller('api/*'), API-key auth) is used by sirloin for service-to-service calls; the Clerk-authenticated /workflow/* controllers serve the human surfaces. Authoring, drafts, sandbox execution, and administrative lists default to ADMIN. Published discovery, contracts, execution, and polling permit CREATOR and above on the explicitly decorated endpoints.

HTTP surfaces and auth

Ordinary sirloin service-to-service calls use the api module (Beef surface) with an API key. The explicit exception is sirloin’s Foxy360 / AgentOS tool surface: those tools call Clerk-authenticated workflow endpoints and forward the caller’s token rather than impersonating an admin. Do not point other sirloin backend flows at Clerk-authenticated controllers.

Contract changes and generated clients

The OpenAPI spec served at /api-json is the source for two committed, generated clients: the Go client sirloin consumes (apps/sirloin/pkg/brain-client/, regenerated with make generate-brain) and the fennec zod client (apps/fennec/src/lib/backend/generated/brain/, regenerated with make generate-brain-fennec-client). They do not regenerate automatically. After any Swagger-visible controller or DTO change, restart Brain so the live spec refreshes, regenerate both clients from the repo root, inspect the generated diffs, and validate affected consumers. See apps/brain/AGENTS.md for the sequence.

Inference, moderation, pricing

  • Adapters — one generic integration per inference provider (FAL, WaveSpeed, RunPod, OpenRouter, Hive); the model is an input on the inference node, not its own adapter. A model that breaks the per-provider assumption can get a model-specific adapter.
  • Moderation wrap — Hive visual moderation on an inference node’s inputs and outputs, enforced by the engine; a workflow disables it only via an explicit flag when its semantics require it (e.g. is_nsfw = true).
  • Pricing — rules are embedded in the contract, but the charge authority stays in sirloin; brisket shows the live price, sirloin re-evaluates server-side before deducting credits.
  • Generation mapping — normal media executions produce a Generation with generation_type = WORKFLOW, workflow_name, and workflow_version, keyed by the media_id in the trigger inputs. Sandbox runs omit it unless create_generation is explicitly requested and supported.

Execution timeouts

The executor layers three budgets. Every wrapper is clamped to the remaining whole-run deadline so a single node cannot outlive the execution.

BudgetDefaultNotes
Whole-run (totalTimeoutMs)60 minHard ceiling for the execution (apps/brain/src/modules/domain/workflow/engine/executor.service.ts:144-145).
Ordinary per-node (nodeTimeoutMs)5 minDefault executor wrapper for nodes that do not declare their own budget.
call_model poll / wrapper10 minOwned by the node’s timeout_ms / max_poll_ms inputs. Unset max_poll_ms follows timeout_ms. Shared helpers live in apps/brain/src/modules/domain/workflow/nodes/gen/call-model-timeouts.ts:1-16.

Long-running nodes opt in via BaseNode.executionTimeoutMs:

  • call_model — returns its own poll budget so the ordinary 5 min node ceiling cannot preempt a documented 10 min (or author-raised) inference wait.
  • subworkflow — uses the parent’s remaining deadline and passes totalTimeoutMs + signal into the nested ExecutorService.execute, so child inference and persistence cancel when the parent budget ends (not a fresh 60 min child window).

Not in scope for v1: HTTP API-triggered cancellation of in-flight executions (engine deadline enforcement and abort-signal propagation, including into subworkflows, are supported), a first-class Templates concept, and brain-owned secrets management.

Module layering and DTO conventions

These are standing architectural decisions; follow them when adding or reviewing code.

  • Domain services are DTO-unaware. Services under modules/domain/*/services/ operate on domain interfaces (e.g. IExample) and throw transport-agnostic domain exceptions (see Brain error handling). They never import request/response DTO classes.
  • Mapping happens at the edge. Controllers translate request DTOs into domain-shaped inputs inline and build responses via static, synchronous Response.toDto(entity) methods on the DTO class. Canonical example: apps/brain/src/modules/domain/generation/controllers/examples.http.controller.ts.
  • The api module is a presentation layer, and is a deliberate deviation. modules/domain/api/ owns the Beef (sirloin-facing, API-key) surface. When building a wire DTO requires async cross-module reads — something a static toDto cannot do — the mapping lives in an injectable presenter/assembler service inside the api module (e.g. BeefExampleService, which decorates examples with their workflow contract and family variant, memoizing lookups per request). This keeps domain services DTO-unaware and controllers thin; the DTO-awareness is confined to the layer whose only job is producing the wire format. Do not replicate this pattern inside domain modules — if a domain module needs composition, compose domain objects there and map in the controller or api layer.
  • Cross-module access goes through exported services, not repositories. A module reaches into another module via that module’s service layer (e.g. WorkflowService.getLatestPublished), never by injecting the other module’s repository directly. Repositories are module-private so caching, validation, and access rules stay in one place.

Workflow examples: input visibility/editability

Per-input visible/editable flags for workflow-backed examples are not stored on the example. They live in WorkflowExampleConfig (one row per workflow, edited in fennec’s Workflow Example Config page) and are resolved live when brain serves examples to sirloin: BeefExampleService ships a top-level workflows: { [name]: { contract, input_overrides } } dictionary covering each example’s own workflow plus all of its family’s member workflows. Consequences:

  • Config edits apply retroactively to every example of that workflow, including sirloin’s hidden-value strip (retroactive IP protection) and authored-value injection at dispatch.
  • Family-backed examples get per-variant flags: the frontend picks the entry for the workflow actually resolved by the user’s axis selection.
  • A missing config means all inputs are visible and editable.
  • There is no per-example snapshot: the former generation_example.input_overrides column was dropped before the feature reached production, and the config is the single source of truth.

Generation and provider decisions live under docs/src/content/docs/decisions/ as they are migrated.

Operations

Brain uses BullMQ/Redis for async generation workflows and PostgreSQL via Prisma for the fennec schema.

Media generation paths that handle provider videos and source media use disk-backed temporary files instead of whole-file buffers where practical. The storage module owns temporary directory cleanup and bounded file I/O through TempFileService and FileIoLimiterService; video persistence, provider preupload, reference-video trimming, and first-frame extraction use path/stream APIs in apps/brain/src/modules/application/storage/services/ and apps/brain/src/modules/application/video/services/.

File I/O limiter state is exported as OpenTelemetry gauges for operations checks; see Observability.

Local Commands

  • cd apps/brain && pnpm lint
  • cd apps/brain && pnpm tsc
  • cd apps/brain && pnpm test
  • cd apps/brain && pnpm start:dev