Architecture

Architecture

Brioso is a distributed system for generating audio: a web app (TanStack Start) handles
intake, brand/prompt analysis, approval, job dispatch, and Stripe payment for three
product lines — an agency Audio Brand Kit, creator custom generation, and a royalty-free
catalog, all at fixed prices (see prd.md’s “Where the numbers live”), plus a Pro
subscription for volume buyers. A kit or track is a parent record that fans out into one
child job per asset once approved — each child job flows through the same generation
pipeline regardless of which product line created it. Generation runs on worker hardware:
a push daemon on a home GPU (pm2-supervised), or an on-demand Modal FunctionCall.
Dispatch is push-only (BRI-439): the sweep POSTs each job to the runtime’s push
endpoint and walks the gateway document’s fallbackChain on retryable failures.
The worker, cloud handler, and wire protocol below are asset-agnostic — they
run one job at a time and don’t know which product line created it.

System topology

 CUSTOMER PATH
 ─────────────
 Customer browser
       │
       ▼
 ┌──────────────────────────────────────────┐
 │ apps/web                                 │
 │ TanStack Start: product UI, API, billing │
 └───────┬────────────────┬─────────────────┘
         │                └──────────────────────► Cloudflare R2
         │                                         uploads and delivery
         ▼
 ┌──────────────────────────────────────────┐
 │ Brioso application database               │
 │ jobs, inference_routes, provider key rings│
 └───────┬──────────────────────────────────┘
         ▼
 ┌──────────────────────────────────────────┐
 │ apps/web in-process sweeps                │
 │ dispatch sweep: every 60 seconds         │
 │ reaper: every 60 seconds                 │
 │ stripe-doctor: daily                     │
 └───────┬──────────────────────────────────┘
         ▼
 ┌──────────────────────────────────────────┐
 │ packages/dispatch                         │
 │ push dispatch, chain fallback, reaper,   │
 │ spend guardrails and observability        │
 └───────┬──────────────────────────────────┘
         ▼
 ┌──────────────────────────────────────────┐
 │ Gateway fallback chain                    │
 │ example: home if healthy else modal       │
 │ disabled or unconfigured runtimes: skip   │
 └───┬──────────────────────────┬───────────┘
     ▼                          ▼
 ┌──────────┐              ┌────────┐
 │ Home GPU │              │ Modal  │
 │ daemon   │              │ app    │
 └────┬─────┘              └───┬────┘
      └──────────┬─────────────┘
                 ▼
   ┌─────────────────────────┐
   │ SA3 / audiocpp handler  │
   │ generation → presigned  │
   │ R2 upload → writeback   │
   └─────────────────────────┘

 ADMIN AND OBSERVABILITY PATH
 ────────────────────────────
 Operator ─► tailnet-only Caddy ─┬─► /dash: PocketBase → Admin Console SPA
                                 │             │
                                 │             └─► internal web API
                                 ├─► db.usebrioso.com: Drizzle / DB tooling
                                 └─► ops.usebrioso.com: Arize Phoenix

 apps/web, dispatch, and workers ─► PostHog + Arize Phoenix
                                    lifecycle events, traces, and eval signals

apps/web’s in-process sweeps are the production periodic triggers (dispatch
60s, reaper 60s, stripe-doctor daily) — Hatchet was removed in BRI-438.
PocketBase serves and proxies the tailnet-only admin console; it does not
own Brioso business logic or the application database.

Job lifecycle

Agency or creator → apps/web → application database: create parent record + jobs
                                      │
                                      ▼
                         in-process dispatch sweep (60s)
                                      │
                                      ▼
                 reaper → push chain walk → runtime submit
                                      │
                                      ▼
        POST job → home daemon / Modal function → R2 writeback
                                      │
                                      ▼
                  Stable Audio 3 generation → R2 upload → completion
                                      │
                                      ▼
        application database: job terminal → parent ready → gated delivery

Modal reports results via the FunctionCall the reaper polls (stale-call
arm); the home daemon PATCHes paid directly. A failed runtime promotes
the job through the configured fallback chain without changing the
product-level lifecycle.

Creator subsystems

The creator flow (/studio/creators) is a secondary, standalone flow — same jobs
table and dispatch pipeline as kit generation, with a few loop-specific pieces:

  • g<N> generation namespacing. Every jobs.tracks entry carries a gen field
    (0 for the first generation, incrementing once per recreate). R2 keys are namespaced
    <slug>/g<gen>/<filename> (server/jobs/handlers.ts issueUploadUrls) and idx stays
    globally monotonic across a job’s whole history (JobsQueue.completeGeneration
    offsets incoming indices and appends rather than replacing) — so a purchased g0 track’s
    assets are never touched by a later recreate’s g1 generation, and createCheckout /
    downloadTrack / the Stripe webhook all key off a single flat idx with no gen-aware
    branching.
  • Recreate (JobsQueue.startRecreate, POST /jobs/:slug/recreate). One atomic
    UPDATE ... WHERE carries every guard (not in flight, 30s cooldown, recreate-count cap —
    3 while unpaid, 10 once any track is purchased) so two concurrent recreate calls can’t
    both pass. Clears sa3_prompt so the dispatch sweep re-refines the revised brief.
  • Email (packages/notify). Provider-neutral (EmailProvider interface, Brevo the only
    implementation today) with sendOnce(db, slug, kind, gen, build) as the idempotency
    layer — an INSERT ... ON CONFLICT DO NOTHING RETURNING against sent_emails wins the
    race before build() runs, so the webhook and the reaper (both call
    onGenerationComplete) can’t double-send the proposal-ready email, and a Stripe webhook
    retry can’t double-send a receipt.
  • License PDF (packages/licensing). Rendered on demand (pdf-lib, no external
    dependency) from docs/track-license.md’s content assembled as a TS template, uploaded
    to the same deterministic <slug>/g<gen>/license-<idx>.pdf key downloadTrack re-derives
    for a repeat download.
  • Feedback (job_feedback table, POST /jobs/:slug/feedback). A DB table rather than
    veena’s R2 JSON records, so it’s queryable from admin tooling directly.
  • Abuse resistance (packages/jobs/src/freeGenerations.ts). A free_generations daily
    counter keyed on normalized email and IP independently (both must clear their cap),
    plus reuse-in-flight (a repeat submission from the same email while a job is still
    new/composing returns the existing job) — see prd.md’s $99 margin-risk note for the
    cost pressure this guards against.
  • Audio protection. Preview streams are watermarked (Pocket TTS, band-specific ducking,
    jittered per-track offsets — masters are never watermarked) and served through a proxy
    (not a redirect) gated on a Fetch-Metadata check plus a short-TTL HMAC playback token
    (packages/auth-core/src/playback-token.ts), so a bare presigned URL never leaks.
    getJob requires either a session that owns the job or a share_token
    (packages/sharing) — a stored, non-expiring, non-revocable per-job/per-track credential
    minted at job creation and threaded through both customer emails and the status page URL,
    since the product is deliberately link-shareable with no login required. /s/:token is a
    read-only listen page for forwarding a single proposal without exposing the full job.

Catalog subsystem

The royalty-free catalog (/music, BRI-125) is pre-generated and browsed rather than
commissioned per brief — a materially different shape from the job/kit pipeline above:

Catalog tables, bucket split, and storage keys live in data-model.md.

  • Taxonomy-driven pages. packages/catalog/src/taxonomy.ts defines four browse axes
    (genre, mood, instrument, use-case) as path-based routes
    (routes/_marketing/music/{genre,mood,instrument}/...); apps/web/src/lib/ catalog-seo.ts’s shouldIndexTaxonomyPage keeps a leaf noindex until it has both
    real intro copy and a published track.
  • Checkout and license. packages/verticals/catalog/src/{track,bundle}-checkout.ts
    charge the fixed per-track/bundle price; packages/licensing renders
    docs/catalog-license.md’s non-exclusive terms for this product type, distinct
    from the creator flow’s exclusive-sale license.

Billing and entitlement

packages/billing (subscription-checkout, webhook, pro-credits, entitlement,
pro-price-display, promo) backs the Pro subscription. Entitlement state lives in
the subscriptions DB table, never behind a feature flag — see
.agents/docs/feature-flags.md’s non-goals.

Subscription / entitlement tables, columns, and webhook flow live in data-model.md.

Account deletion

Users can permanently delete their account from Settings -> Account -> Danger Zone.
The server function (apps/web/src/server/account/delete-account-fn.ts) executes in a
single transaction:

  1. Cancel the Stripe subscription via API (if active).
  2. Hard-delete: pro_credit_usage, grants, catalog_favorites, catalog_download_usage, subscriptions, users.
  3. Anonymize: catalog_purchases.user_id set to NULL (preserves sales records).

Orphan-row policy for email-keyed tables lives in data-model.md.

Session invalidation is automatic: the stateless HMAC cookie references a userId
that no longer exists, so the next request resolves to logged-out.

Grace period (soft delete + reaper) and full GDPR erasure (PostHog, analytics,
email anonymization) are deferred to a future iteration.

Repo layout

Path Role
apps/web/ TanStack Start app — brand/prompt intake, strategy, kit/track orchestration, Stripe, admin
apps/worker/ Home GPU push daemon — generation, presigned-R2 upload, writeback
packages/verticals/kits/ Agency Audio Brand Kit vertical: assembly, brand-schema, checkout, strategy AI workflow, state machine
packages/verticals/studio/ Creator studio vertical: UI components, composer/canvas domain; routes served by apps/web at /studio/*
packages/verticals/creator/ Custom generation vertical: recreate, remix, feedback, free-tier quota, refine AI workflow
packages/verticals/catalog/ Royalty-free catalog vertical: track/bundle checkout, delivery
packages/catalog/ Catalog taxonomy, archetypes, slugs — browse-page data, distinct from verticals/catalog’s checkout
packages/classification/ CLAP embeddings + sklearn classifier for catalog QC/enrichment (BRI-142); isolated uv venv, not a [tool.uv.workspace] member
packages/inference-core/ Shared protocol types + SubprocessEngine (JSONL stdout → Python)
packages/musicgen-adapters/ Domain models, ports (Protocols), storage adapters
packages/ai-workflow/ Stateless LLM workflow (refine, brand analysis) — no DB, no obs, no env
packages/jobs/ Job queue + contract schemas (TS side of the Python contract)
packages/entitlements/ Grant codes + entitlement resolver (allowance, plan, redeem)
packages/billing/ Stripe checkout + webhook handling, promo codes
packages/notify/ Email: provider-neutral message shape, Brevo, templates, at-most-once send
packages/licensing/ License PDF rendering (pure pdf-lib, no I/O)
packages/sharing/ Share-link domain model: crypto-random tokens scoped to a kit or track, no expiry/revocation
packages/dispatch/ Job dispatch sweep, spend ledger, reaper
packages/contract/ TS<->Python wire-protocol source of truth (OpenAPI document)
packages/auth-core/ Auth primitives: OTP, session route guards, rate limiting
packages/config/ Env/config loading + audit, injected not read ambient
packages/storage/ R2/S3 surface + presigned upload URLs, over injected credentials
packages/brioso-brand/ Brand tokens, wordmark, shared React primitives
packages/obs-js/ Shared observability facade (TS) — PostHog errors/events/AI-spans + LogTape, OTel export to Arize Phoenix
packages/obs-py/ Shared observability facade (Python) — same facade, absorbs the old musicgen-trace OTel setup
packages/stable-audio-3/ SA3 backend — isolated uv venv, workspace sibling (not a workspace member)
packages/db/ Database schema + migrations
deploy/ Deploy v2: activate binary (bun --compile), pyinfra/v2/ tree, plus systemd units (litestream, Drizzle Gateway), Phoenix podman quadlet, sudoers. The macOS launchd plist is NOT committed — apps/worker/scripts/install-worker.sh generates one per-user at $HOME/Library/LaunchAgents/.
docs/ End-user / product docs

Synced against ls packages/ and ls packages/verticals/ on 2026-08-08. apps/admin,
packages/signature, and packages/musicgen-trace may still appear on disk in some
checkouts as leftover .turbo/node_modules build artifacts — they are not git-tracked
and not part of the real package set; apps/admin was removed per BRI-221 (see Admin/ops
surface below).

See packages/stable-audio-3/scripts/README.md for the standalone generation script and .agents/docs/gpu-gateway-canary.md for the push-dispatch operations doc.

Data model

Tables, storage, secrets, and env live in
data-model.md — the canonical reference. This doc only
mentions table names inline; consult data-model.md for columns, indexes,
storage keys, R2 bucket prefixes, Litestream WAL, and secret/env strategy.
If packages/db/src/schema.ts changes, data-model.md is updated in the
same PR.

Wire protocol

Workers emit a discriminated JSONL stream on stdout. Each line is a single JSON object
with a type field. The orchestrator (SubprocessEngine) parses lines until it sees a
terminal result event, then returns.

{"type": "progress", "stage": "loading_model", "progress": 0.0, "message": ""}
{"type": "progress", "stage": "generating", "progress": 0.5, "message": "step 4 of 8"}
{"type": "result", "status": "ok", "output_path": "/tmp/out.wav", "sample_rate": 44100, "duration_seconds": 30.0}

Event types:

type Key fields Description
progress stage, progress (0–1), message Stage progress update
event `name: “ready” “unloading”, device?`
result `status: “ok” “error”, output_path?, sample_rate?, duration_seconds?, error?`

Legacy compat: a bare GenerateResponse JSON line (no type field) is also accepted —
if a line has status but no type, it is treated as a result event.

Full types: see musicgen_adapters.ports.events.WorkerEvent.

Deployment modes

Mode How it works Guide
VPS web app Deploy v2 (artifact + activate) on a VPS via Tailscale; beta live on v2, prod still v1 until WS5 deploy/README.md
Home worker (macOS/Linux) pm2-supervised push daemon (brioso-worker serve) .agents/docs/worker-deployment.md + .agents/docs/home-worker-wake.md
Modal serverless On-demand GPU FunctionCall, push-dispatched .agents/docs/gpu-gateway-beta-runbook.md

Observability

Signal Source Backend Status
Errors obs-js/obs-py captureError (web, worker, runtimes) PostHog (EU) captureException Live. Replaces GlitchTip — GlitchTip DSN/config removed.
Lifecycle events obs-js/obs-py event, registerHttpObs, JobsQueue transitions PostHog (EU) capture() events Live. Every observed request (/jobs, /dispatch, /kits, /uploads, /otel — see apps/web/plugins/obs.ts) and job state transition emits one, tagged runner.
Refine/LLM spans runPrompt → WorkflowCtx.onGeneration → aiGeneration PostHog $ai_generation Live. Every @brioso/ai-workflow call reports model, latency and token usage; failures too. The package stays vendor-neutral, apps/web maps onto $ai_* (server/ai/workflow-ctx.ts).
Deep OTel spans registerHttpObs root span + withSpan/with_span + ai-sdk → OTLP/HTTP Arize Phoenix, dev + prod (PHOENIX_COLLECTOR_ENDPOINT) Live. One trace spans web → runtime: the request’s traceparent is stamped on the job row (the dispatcher’s leased_by push is out-of-band, so the row carries the context), and worker writebacks inject it back. Two span processors — OpenInference for ai-sdk spans, a generic one for everything else, since the OpenInference filter drops manual spans. Silent no-op when Phoenix is unreachable or TRACING_ENABLED=false. Agent-queryable via Phoenix’s native /mcp HTTP endpoint (Phoenix 19+) — see .agents/docs/phoenix-runbook.md.
Health GET /health checks DB + R2 + stale queue Edge-triggered PostHog error on ok→failing transition Live. No external prober yet.
Product analytics posthog-js autocapture + session replay PostHog (EU) Live. Product-side, unaffected by this migration.

Database access

Who How Scope
apps/web @libsql/client → DATABASE_URL (file:/opt/brioso/shared/data/local.db) Read-write
apps/worker HTTP to apps/web job endpoints (ApiJobStore); never touches DB directly Via web
Teammate (SQL) Drizzle Studio web UI → db.usebrioso.com (Caddy) → Drizzle Gateway (:4983) → reads local.db Read-only (passcode)
Backup Litestream streams WAL from local.db → R2 (brioso-jobs/litestream/) every 1s Continuous

Drizzle Gateway is a viewer, not a DB server — the web app keeps direct file access, Drizzle Gateway reads the same SQLite file as a second WAL reader. No cutover risk, no Litestream conflict. A future migration to Turso managed is still a single DATABASE_URL change for the web app.

Admin / ops surface

apps/admin (TanStack Start) is removed (BRI-221); its interim replacement
(self-hosted Windmill) was also removed 2026-08-26. There is no dedicated
admin UI — see .agents/notes/2026-08-26-windmill-admin-followup.md. Do not
invest in new admin-specific infra without checking current intent first.

Where this is headed

See roadmap.md for the roadmap — Iteration/Quarter milestones, open issues, and
deferred work.