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. Everyjobs.tracksentry carries agenfield
(0for the first generation, incrementing once per recreate). R2 keys are namespaced
<slug>/g<gen>/<filename>(server/jobs/handlers.tsissueUploadUrls) andidxstays
globally monotonic across a job’s whole history (JobsQueue.completeGeneration
offsets incoming indices and appends rather than replacing) — so a purchasedg0track’s
assets are never touched by a later recreate’sg1generation, andcreateCheckout/
downloadTrack/ the Stripe webhook all key off a single flatidxwith no gen-aware
branching.- Recreate (
JobsQueue.startRecreate,POST /jobs/:slug/recreate). One atomic
UPDATE ... WHEREcarries 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. Clearssa3_promptso the dispatch sweep re-refines the revised brief. - Email (
packages/notify). Provider-neutral (EmailProviderinterface, Brevo the only
implementation today) withsendOnce(db, slug, kind, gen, build)as the idempotency
layer — anINSERT ... ON CONFLICT DO NOTHING RETURNINGagainstsent_emailswins the
race beforebuild()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) fromdocs/track-license.md’s content assembled as a TS template, uploaded
to the same deterministic<slug>/g<gen>/license-<idx>.pdfkeydownloadTrackre-derives
for a repeat download. - Feedback (
job_feedbacktable,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). Afree_generationsdaily
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) — seeprd.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.
getJobrequires either a session that owns the job or ashare_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/:tokenis 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.tsdefines 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’sshouldIndexTaxonomyPagekeeps a leafnoindexuntil 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/licensingrenders
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:
- Cancel the Stripe subscription via API (if active).
- Hard-delete:
pro_credit_usage,grants,catalog_favorites,catalog_download_usage,subscriptions,users. - Anonymize:
catalog_purchases.user_idset 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.