Data Model Overview
This page is the spine for data knowledge across the monorepo: where state lives, who owns each table, how schemas migrate, and how data flows between services. For per-domain entity detail, see the companion Data Models page.
Persistent Stores
| Store | Owner | Connection | Env vars |
|---|---|---|---|
Postgres — sirloin DB | sirloin (BUN ORM); brain owns the fennec schema inside it via Prisma | Single Postgres host (rump:5432 in dev) | SIRLOIN_DATABASE_URL, BRAIN_DATABASE_URL, DATABASE_URL, DIRECT_DATABASE_URL |
| Object storage — S3-compatible | sirloin (primary), brain (R2 endpoint) | MinIO in dev (tenderloin:9000); Cloudflare R2 in prod (per BRAIN_AWS_S3_APIURL) | SIRLOIN_S3_ENDPOINT, SIRLOIN_S3_BUCKET, SIRLOIN_S3_ACCESS_KEY, SIRLOIN_S3_SECRET_KEY, SIRLOIN_S3_PUBLIC_URL, BRAIN_AWS_S3_APIURL, BRAIN_S3_BUCKET_NAME, BRAIN_PUBLIC_BUCKET_URL |
| Redis | brain (BullMQ queues) | Co-located with brain | REDIS_HOST, REDIS_PORT, REDIS_PASSWORD (apps/brain/src/config/queue.config.ts:8-10); dev wiring docker-compose.yml:263-264 |
| Neon (managed Postgres) | Production hosting layer for the Postgres database above | Branch-per-PR via .github/workflows/neon-branching.yml | TODO(@law): production DATABASE_URL shape (Neon connection string) |
| Chargebee | sirloin (read-through, not a local store) | HTTPS API | SIRLOIN_CHARGEBEE_* (see apps/sirloin/Makefile) |
Evidence: env var names from apps/brain/.env.example and
apps/sirloin/.env.example; Redis queue usage from
apps/brain/src/modules/bullBoardQueues.module.ts and
apps/brain/src/modules/domain/character/character.module.ts; Neon branching
from .github/workflows/neon-branching.yml.
Schema Ownership
Brain and sirloin share a single Postgres instance but own disjoint schemas. They never cross-query — all reads cross the gRPC boundary.
Postgres (rump:5432)├── schema: public, users, characters, media, billing, bi, audits,│ autoscaling, mcp ← owned by sirloin (BUN)└── schema: fennec ← owned by brain (Prisma)| Schema | ORM | Owner | Source |
|---|---|---|---|
users, characters, media, billing, bi, audits, autoscaling, mcp | BUN | sirloin | apps/sirloin/internal/pkg/models/ (users.go, characters.go, media.go, bi.go, etc.) |
fennec | Prisma | brain | apps/brain/prisma/schema.prisma |
| Strip | none — read-only over gRPC | strip | No DB; consumes sirloin gRPC. Confirmed: strip imports no database/sql/bun.DB driver — StripAuditLog rows in strip code are proto-generated types from apps/strip/internal/pkg/pb/sirloin/v5/strip.pb.go:3618. |
Per-Service Entity Overview
sirloin (BUN, schemas above)
Top entities, sourced from apps/sirloin/internal/pkg/models/*.go:
| Entity | Purpose | Key relations |
|---|---|---|
User (users.go) | Identity projection keyed by Clerk ID | → Credit, Character, Media |
Credit | Standard + full-access credit balances | → User, Purchase, Referral |
Character (characters.go) | Runtime state, reservation, shop | → Media, ReferenceDatasetImage |
Media (media.go) | Generated assets, favourites, archive | → Character, Example |
ReferenceDatasetImage | Training reference rows | → Character |
DunningAttempt | Failed-invoice retries | → billing |
FailedOperation (failedoperation.go) | Saga retry queue | (see ADR 2026-04-27-saga-pattern-for-distributed-payments) |
FraudFlag, FraudCooldown, FraudEvent, HighRiskBin, CardFingerprintAccount | Fraud signals | → User |
PrimerPollingState (primer_polling_state.go) | Cursor for Primer settled-payment polling | (see ADR 2026-04-27-primer-payment-gateway) |
FlankWorkflow, FlankExecution, FlankAdapter | Workflow engine state | (flank service) |
MonitorProbe | Synthetic monitoring | — |
StripAuditLog, StripAdminRole | Admin audit + RBAC | → User |
brain (Prisma, schema fennec)
Top entities, sourced from apps/brain/prisma/schema.prisma:
| Entity | Purpose | Key relations |
|---|---|---|
User | Identity record | → Character, Generation |
Character | Detailed appearance + training metadata | → OnboardingImages, BodyType, CharacterControlGroup, TrainingJob |
Generation | Permanent generation record (prompts, logs) | → Character, GenerationExample, GenerationTag |
TrainingJob | LoRA training lifecycle | → Character |
DatasetGenerationRequest | RunPod dataset job | → Character |
OnboardingImages | Validated onboarding inputs | → Character |
Pack, Item, ShopVIImportJob | Shop catalogue + import | → Character |
Settings, CharacterSettings, ApplicationSettings, WappySettings, IfastConfig, Preset | App + character configuration | — |
InferenceProviderSpend | Per-provider spend tracking | unique on (date, inferenceProvider) |
ImageModerationResult, ImageModerationTag | Moderation outcomes | — |
MotionType, VirtualCharacterTemplate, GenerationExample, GenerationTag | Generation reference data | — |
Cross-Service References
The two services hold soft references to each other’s identifiers; there are no enforced FKs across the schema boundary.
| From | To | Mechanism | Notes |
|---|---|---|---|
Brain.User.clerkId | Sirloin.User.clerkId | Shared Clerk ID | Both sides keyed by the Clerk subject |
Brain.Character.id | Sirloin.Character.id | Same UUID, written through gRPC | sirloin owns runtime state, brain owns appearance |
Brain.Generation.characterId | Sirloin.Character.id | UUID match | sirloin → brain via apps/sirloin/pkg/brain-client/ |
Sirloin.Media.characterId | Brain.Character.id | UUID match | — |
Chargebee customer_id | Sirloin.SubsAll.customerId (bi schema) | External-system FK | sirloin pulls per ADR 2026-04-27-chargebee-polling-over-webhooks |
Confirmed: apps/brain/prisma/schema.prisma contains no
subscriptionId/invoiceId/chargebee columns — brain holds no
sirloin-side billing FKs.
Data Flow
1. Signup → customer creation
sequenceDiagram autonumber participant U as User participant Brisket as brisket (Next.js) participant Clerk participant Sirloin as sirloin (Go) participant Brain as brain (NestJS) participant CB as Chargebee
U->>Brisket: Sign up Brisket->>Clerk: OAuth / email auth Clerk-->>Brisket: clerkId, JWT Brisket->>Sirloin: REST /users (JWT) Sirloin->>Sirloin: upsert User + Credit (clerkId) Sirloin->>Brain: gRPC EnsureUser(clerkId) Brain->>Brain: upsert fennec.User Sirloin->>CB: create Customer CB-->>Sirloin: customer_id2. Generation request → media row
sequenceDiagram autonumber participant Brisket participant Sirloin participant Brain participant Round as round (ML) participant S3
Brisket->>Sirloin: REST GenerateMedia Sirloin->>Sirloin: debit Credit (txn) Sirloin->>Brain: gRPC StartGeneration(characterId, prompt) Brain->>Brain: enqueue BullMQ job + insert Generation Brain->>Round: gRPC inference call Round-->>Brain: image bytes / embedding Brain->>S3: upload asset Brain-->>Sirloin: gRPC NotifyMediaReady(mediaId, path) Sirloin->>Sirloin: insert Media (status=COMPLETED)3. Payment → subscription state
sequenceDiagram autonumber participant CB as Chargebee participant Sirloin participant Brain Note over Sirloin: Pull-based, not webhooks (ADR 2026-04-27-chargebee-polling-over-webhooks) Sirloin->>CB: poll subscription / invoice events CB-->>Sirloin: events Sirloin->>Sirloin: distributed lock per customer (ADR 2026-04-27-distributed-locks-for-payments) Sirloin->>Sirloin: write SubsAll (bi), DunningAttempt, FailedOperation Sirloin->>Sirloin: defer activation if needed (ADR 2026-04-27-deferred-subscription-activation) Sirloin->>Brain: gRPC update entitlements (if any)Migration Discipline
| Service | Tool | Source of truth | Notes |
|---|---|---|---|
| brain | Prisma migrate | apps/brain/prisma/migrations/ (timestamped, e.g. 20241231153339_init_fennec) | Prisma client generated on start:dev (prisma generate && nest start --watch, apps/brain/package.json). Migrations are applied at container boot via pnpm exec prisma migrate deploy (apps/brain/startup.sh:9); CI (.github/workflows/brain.yml) only runs prisma generate for lint/typecheck/test, no migrate deploy step. |
| sirloin | Custom Go runner | apps/sirloin/internal/app/migrate/migrate.go, SQL files in apps/sirloin/internal/app/migrate/schema/*.sql | make verify-migrations checks numbers are unique (apps/sirloin/Makefile); CI gate ci-all runs it. |
| strip, round, fennec, brisket, chuck, shank, flank | n/a | — | No owned schema. |
Branch-per-PR Postgres copies are provisioned by
.github/workflows/neon-branching.yml.
Backups, Retention, PII
- TODO(@law): production backup schedule and RPO/RTO targets.
- TODO(@law): point-in-time recovery window on Neon.
- TODO(@law): retention policy for
StripAuditLog,FraudEvent,Generationrows. - PII surface:
Brain.User.email/firstName/lastName,Sirloin.User,Sirloin.SubsAll.email, Chargebee customer records, S3 generated assets. See Security Model.
Cross-References
- Companion entity detail: Data Models
- ADRs that constrain data flow:
2026-04-27-distributed-locks-for-payments,2026-04-27-saga-pattern-for-distributed-payments,2026-04-27-deferred-subscription-activation,2026-04-27-chargebee-polling-over-webhooks,2026-04-27-primer-payment-gateway - Runtime topology: Runtime Topology
Open follow-ups
- Production
DATABASE_URLshape on Neon (TODO(@law)). - Production backup schedule, RPO/RTO targets (TODO(@law)).
- Neon point-in-time recovery window (TODO(@law)).
- Retention policy for audit, fraud, generation rows (TODO(@law)).