Skip to content

Data Models

High-level guide to the data models across the monorepo. For full field-level details, refer to the Prisma schema and BUN model definitions directly.

A local PostgreSQL server (rump:5432) hosts separate databases:

Database/Schema ORM Owner Service Purpose
rump (multiple BUN schemas) BUN sirloin Billing, user credits, runtime state, admin, fraud
fennec (fennec schema) Prisma brain Workflows, generation records, character details, training, config

Brain and Sirloin may share a PostgreSQL server locally, but own separate databases/schemas and never cross-query. Sirloin calls Brain over HTTP; both use stable domain IDs rather than cross-database constraints.

Each domain entity has a single source of truth. Some entities exist in both services with different responsibilities.

Source of truth: sirloin (credits/billing), brain (identity)

Brain.User (id, clerkId, email, firstName, lastName)
↕ synced via Clerk ID
Sirloin.Credit (userId, standardCredits, fullAccessCredits, referralCode)
→ Sirloin.Purchase (itemPriceId, transactionId, price, currency)
→ Sirloin.Referral (referringUserId, referredUserId, creditsEarned)

Source of truth: sirloin (status, shop/reservation), brain (detailed attributes, training)

Sirloin tracks runtime state: status, availability, reservation, purchase, KYC verification, credit spend. Brain tracks detailed appearance attributes, onboarding images, body type, lora settings, and training jobs.

Sirloin.Character
status, availabilityStatus, reservedBy, reservedUntil, purchasedBy, priceTier
→ Sirloin.ReferenceDatasetImage (imagePath, etag)
Brain.Character
characterName, gender, ethnicity, hairColor, bodyType, lora, style, trigger
→ Brain.OnboardingImages (imageUrl, validationMethod, analysis)
→ Brain.BodyType (uiLabel, gender, defaultPromptValue)
→ Brain.CharacterControlGroup (name)

Source of truth: sirloin (user-facing state), brain (generation records)

Sirloin owns favorites, archives, download tracking, and cost. Brain owns the permanent generation record with prompts, logs, and model parameters.

Sirloin.Media
userId, characterId, status, isFavorite, isArchived, cost, controls
→ Sirloin.Example (description, descriptionVersion, embedding, embeddingVersion,
characterModels[], explicitnessLevel)
→ Sirloin.Tag (via ExampleTag join table)
Brain.Generation
genId, characterId, prompt, success, mediaType, logs, inputVariables
→ Brain.MotionType (name, prompt, lora)
→ Brain.GenerationExample → Brain.GenerationTag

Sirloin versions generated descriptions and embeddings independently. A source checksum change clears both artifacts. The maintenance workers also rebuild any row whose stored version differs from its current media-specific contract.

Descriptions use the GA gemini-3.7-flash model through OpenRouter, pinned to the google-vertex provider with provider fallbacks disabled. Image and carousel examples send up to four source images. Video examples send a four-frame spritesheet even when the row also has a thumbnail. The video prompt describes observable motion, pose changes, camera movement, and scene changes across those frames. The worker does not upload full videos. Full-video input adds transfer, token, privacy, and latency costs and needs a task-specific retrieval evaluation before adoption. Further model or provider changes also require retrieval evaluation. Description batches contain up to 25 rows to leave headroom within the five-minute task timeout; maintenance runs every five minutes.

The document embedding combines the visual description with curated tags, location, and media type. It excludes generation prompts because the generated media can differ from the request. Search queries use BGE’s query instruction, while stored documents do not.

If Gemini returns no text after safety filtering, including for legal adult content, Sirloin records a current, empty description. The embedding then uses curated tags, location, and media type. This terminal fallback keeps rollout readiness measurable without adding unobserved visual claims. Malformed or truncated provider responses remain pending even after retries are exhausted; they are not recorded as successful metadata-only fallbacks. Disabling configurable thresholds does not bypass provider-level safety policy.

The legacy search worker applies the same metadata-only fallback to two failures that repeat on every attempt. In the first, the example has no video and none of its carousel images can be used: each is empty or larger than the 16 MiB download cap. The worker tries carousel paths in order until four images download, so a later image replaces an earlier one that fails. In the second, OpenRouter rejects the request as too large (HTTP 413). The worker then logs a warning, plus one warning per unusable image, and publishes the fallback once instead of retrying every 30 minutes. Other failures keep the 30-minute retry, including a carousel download that fails for another reason, rate limits, and provider errors.

Vector search applies access, tag, media-type, Kitsune, welcome, and example-ID filters in PostgreSQL. A materialized statement-local setting enables strict iterative HNSW scanning through transaction-pooled connections. The HNSW candidate query is LATERAL and depends on that setting, so PostgreSQL applies it before the index scan starts. Interactive skip_total requests avoid separate BEGIN, SET LOCAL, and COMMIT round trips. Exact-count requests keep a read-only repeatable-read transaction. A bounded inner query orders by distance so PostgreSQL can use HNSW. An outer query adds an ID tie-breaker over the bounded candidates. The default contract returns an exact total from the same snapshot. Interactive feeds set skip_total and paginate with has_more. Sirloin also caches successful query embeddings for 15 minutes by SHA-256 key. The cache does not retain raw query text and does not cache failures. Unbounded requests return the materialized row count. Verify pgvector 0.8.0 or newer before rollout because older versions do not support strict iterative scans; schema migrations do not enforce this version requirement. An embedding-service failure returns an unavailable error. It never changes semantic search into an unfiltered catalog request.

Three flags guard the vector-space migration. New semantic fields do not replace the legacy description, embedding, or readiness fields during rollout.

  1. Deploy Round first. Requests without a pooling option keep mean pooling.

  2. Deploy Sirloin with backfill, cutover readiness, and search flags false. Wait for old instances.

  3. Enable SIRLOIN_EXAMPLE_SEMANTIC_V2_BACKFILL_ENABLED. Keep search disabled.

  4. Wait until this query returns zero eligible rows:

    SELECT count(*)
    FROM media.examples
    WHERE deleted_at IS NULL
    AND (
    image_path <> ''
    OR cardinality(coalesce(image_paths, '{}')) > 0
    OR coalesce(video_path, '') <> ''
    OR coalesce(public_video_path, '') <> ''
    )
    AND (
    semantic_description_version IS DISTINCT FROM CASE
    WHEN type IN ('VIDEO', 'MEDIA_TYPE_VIDEO')
    OR media_type IN ('VIDEO', 'MEDIA_TYPE_VIDEO')
    THEN 'openrouter-gemini-3.7-flash-observable-video-v3'
    ELSE 'openrouter-gemini-3.7-flash-observable-v7'
    END
    OR semantic_embedding_version IS DISTINCT FROM 'bge-small-en-v1.5-cls-v2'
    );
  5. Set SIRLOIN_EXAMPLE_SEMANTIC_V2_CUTOVER_READY=true to record the completed zero-backlog check.

  6. Enable SIRLOIN_EXAMPLE_SEMANTIC_V2_SEARCH_ENABLED.

During backfill, search reads the intact legacy embedding column. After cutover, search reads only current semantic CLS vectors. Configuration rejects search cutover unless backfill remains enabled. Before serving the new legacy search path, run apps/sirloin/ops/semantic_search_fallback_indexes_concurrent.sql to prepare its HNSW and tag-first join indexes, including on an empty database. The concurrent operation repairs mismatched indexes as well as interrupted builds; a valid same-named index alone is not sufficient. Index readiness is an operator prerequisite, not a schema migration gate. See the index rollout procedure.

For rollback, disable the search flag first. Reads immediately return to the legacy vector column. Then clear the cutover-ready flag, disable backfill, and wait for active jobs to stop. Do not clear either vector column during rollback.

Source of truth: sirloin (all billing)

Sirloin.DunningAttempt — payment retry attempts for failed invoices
Sirloin.FailedOperation — billing ops needing retry (cancel, apply_credits, rollback, etc.)
Sirloin.SubsAll (bi schema) — Chargebee subscription sync (email, customerId, mrr, planUnitPrice)
Sirloin.FraudEvent — fraud signal audit log
Sirloin.FraudFlag — persistent per-user fraud status
Sirloin.FraudCooldown — velocity rule blocking
Sirloin.HighRiskBin — high-risk BIN list
Sirloin.CardFingerprintAccount — maps card fingerprints to user accounts
Sirloin.PrimerPollingState — cursor for Primer settled payment polling

Source of truth: sirloin (reservations/purchases), brain (catalog metadata)

Sirloin.Character fields: priceTier, availabilityStatus, catalogueImagePaths,
reservedBy, reservedUntil, reservationToken, purchasedBy, purchasedAt
Brain.ShopVIImportJob (directoryPath, status, stats)

Source of truth: sirloin

Sirloin.StripAuditLog (userId, action, entityType, entityId, changes, metadata)
Sirloin.StripAdminRole (userId, role: root|admin|creator|support|viewer)

Brain keeps its own append-only trail for safety-threshold and workflow publishing changes, with a deliberately matching column set:

Brain.AuditLog (userId, userEmail, action, entityType, entityId, changes, metadata)

See Compliance Audit Trail.

Source of truth: sirloin

Sirloin.MCPThread → MCPRun → MCPRunStep
→ MCPArtifact
→ MCPConnectorCall
MCPMessage (role: user|assistant|system|tool)

Brain owns application config: Settings, Preset, ApplicationSettings.

Sirloin owns infrastructure config: ScalingDecision (autoscaling decisions with worker counts, prediction methods, spike detection).

Type Examples Notes
JSONB Media.controls, FailedOperation.operationData, FraudEvent.metadata, StripAuditLog.changes Structured metadata, varies per record
Arrays Character.captions[], Example.characterModels[], Character.catalogueImagePaths[] PostgreSQL native arrays
Vector Example.embedding Used for semantic search over examples
Soft deletes deletedAt on Character, Media, ReferenceDatasetImage BUN and Prisma both use soft delete patterns

Brain (Prisma):

  • Generation(characterId), Generation(characterId, success), Generation(mediaType, createdAt DESC, success) — generation queries
  • Generation(modelUrls) GIN index — array filtering
  • OnboardingImages(characterId, createdAt DESC) — failed images query
  • InferenceProviderSpend(date, inferenceProvider) unique — spend dedup

Sirloin (BUN):

  • Standard FK indexes on userId, characterId, mediaId
  • Billing tables indexed on invoiceId, customerId, subscriptionId
  • Fraud tables indexed on userId, cardFingerprint, bin6

Proto messages in proto/sirloin/v5/ map closely to BUN models but are not 1:1. Key differences:

  • Proto Character message is a projection — combines fields from both sirloin and brain character records
  • Proto Media message includes computed fields (thumbnail_url, video_preview_url) not stored in DB
  • Proto ShopCharacter is a specialized view of Character with shop-specific fields
  • Proto billing messages (SubscriptionDetails, Product, CouponInfo) aggregate data from Chargebee + local billing tables
What Path
Brain Prisma schema apps/brain/prisma/schema.prisma
Sirloin BUN models apps/sirloin/internal/pkg/models/
Proto definitions proto/sirloin/v5/, proto/round/v1/
Generated proto docs api/server.md