Local Stack — Home Worker + Garage S3
Local Stack — Home Worker + Garage S3
Run the full Brioso stack locally with the home GPU worker and a local Garage S3
bucket, without touching real Cloudflare R2.
This covers BRI-199 (runner routing + Garage S3). Garage is bootstrapped via
Docker directly.
Overview
apps/web (TanStack Start)
├── POST /jobs → jobs.runner = "home" (when JOBS_DEFAULT_RUNNER=home)
└── dispatch sweep → POST to the daemon's /jobs (push; see
home-worker-wake.md)
│
├── SA3 generation (MLX on Apple Silicon, diffusers on CUDA)
└── PUT to Garage (presigned PUT URLs arrive inside the push
payload — no worker-side R2 credentials)
Jobs default to the gateway chain head in production. JOBS_DEFAULT_RUNNER=home
routes every new job to the home worker.
Prerequisites
mise trust && bun install # one-time per worktree
uv pip install -e apps/worker
uv pip install -e packages/stable-audio-3 packages/musicgen-adapters packages/inference-core
# ffmpeg must be on PATH
One-time: Garage S3
Garage on Docker
Start Garage and initialise a bucket and key:
docker run -d --name garage \
-p 3900:3900 -p 3903:3903 \
-v garage-data:/data \
garagehq/garage:v1
# Wait for startup, then initialise a layout (one-time)
docker exec garage garage layout assign /data garage -r 1
# Create the jobs bucket
docker exec garage garage bucket create brioso-jobs
# Create an API key for the web app
docker exec garage garage key create local-dev
# Allow read+write on the bucket
# (replace <key-name> with the name printed by the previous command)
docker exec garage garage bucket allow brioso-jobs \
--key <key-name> --read --write
The key create command prints an access key ID and secret. Copy them for the
config step below.
Garage on bare-metal or with nono sandbox
You need to have it pre-installed.
Configure apps/web
Infisical’s dev environment already provides Garage-pointed R2_* values —
a team-wide, 2-dev convenience setup. Each developer still needs their own
local Garage running, since the endpoint and key are machine-local; point
Infisical dev’s R2_ACCESS_KEY_ID/R2_SECRET_ACCESS_KEY at your own
garage key create output if they differ. Because of this, apps/web/.env
normally doesn’t need its own R2_* block to run bun run dev:infisical.
Set the following in apps/web/.env (needed either way for runner routing
and worker auth; needed for the full R2_* block only when running
bun run dev, with no Infisical):
# ── Runner routing ───────────────────────────────────────────────
# Every new job inserts with this runner when the caller doesn't specify one.
# "home" → the dispatch sweep pushes jobs to the daemon's POST /jobs.
# Unset → home-capacity bias, then the gateway chain head.
JOBS_DEFAULT_RUNNER=home
# Optional: allow POST /jobs callers to pin a specific runner.
# Default-deny; only intended for local dev. Unset to disable.
# JOBS_ALLOW_RUNNER_OVERRIDE=true
# ── Garage S3 (local) ───────────────────────────────────────────
# Only needed for `bun run dev` (no Infisical) — dev:infisical already gets
# these from Infisical's dev environment (see above).
R2_ENDPOINT=http://localhost:3900
R2_REGION=garage
R2_FORCE_PATH_STYLE=true
R2_BUCKET=brioso-jobs
R2_ACCESS_KEY_ID=<from `garage key create`>
R2_SECRET_ACCESS_KEY=<from `garage key create`>
# ── Worker auth ─────────────────────────────────────────────────
# Must match WORKER_SHARED_SECRET set in the worker's environment.
WORKER_SHARED_SECRET=local-dev-secret
Restart the dev server after changing env vars.
Configure apps/worker
The worker reads plain env vars, not Infisical. Set in its .env or shell:
# Auth — must match WORKER_SHARED_SECRET above
WORKER_SHARED_SECRET=local-dev-secret
# Backend to poll
JOBSTORE_API_URL=http://localhost:3000
# Optional: MLX vs diffusers (auto-detected on macOS vs other platforms)
# SA3_ENGINE=mlx
# Optional: HuggingFace token for first-run SA3 weight download
# HF_TOKEN=
# Optional: refine service (no-op if unset)
# BRIOSO_REFINE_URL=
# BRIOSO_REFINE_API_KEY=
# Optional: observability (no-op if unset)
# PHOENIX_COLLECTOR_ENDPOINT=http://localhost:6006
# POSTHOG_API_KEY=
Run the stack (one command)
For the common case (default SA3/MLX model, no flag overrides), skip the
manual two-terminal dance below:
mise run dev:home-worker
This applies pending packages/db migrations, then starts apps/web
(dev:infisical, with JOBS_DEFAULT_RUNNER=home) and apps/worker together,
resolving WORKER_SHARED_SECRET from your shell env
or, failing that, Infisical’s dev project (infisical secrets get WORKER_SHARED_SECRET --env=dev), and reading vite’s actual bound port from
its own stdout (vite auto-increments past :3000 when another worktree
already has a dev server running). It exits immediately if the secret isn’t
set either way — add it to Infisical’s dev project or export
WORKER_SHARED_SECRET yourself first. Logs from both interleave in one
terminal; Ctrl+C stops both. You still need Garage running first (docker start garage, or mise run s3:up). See scripts/dev-home-worker.ts for
exactly what it runs.
If you need to override the model, pin a fixed secret across restarts, or
run Garage/web/worker in separate terminals for closer inspection, use the
manual flow below instead.
Run the stack
# Terminal 1 — Garage S3
docker start garage
# Terminal 1 — web app (from repo root)
JOBS_DEFAULT_RUNNER=home bun run dev:infisical
# Terminal 2 — home GPU worker
uv run brioso-worker worker \
--model sa3 \
--worker-id "$(hostname)" \
--store api
Verifying it works
- Open
http://localhost:3000/dev/login?persona=creator&redirect=/studioto authenticate.
(agencyis a kept alias forcreator; both mint the same session —
canonical persona for studio QA iscreator, see
.agents/docs/dev-seed-states.md.) - Create a job through the studio UI.
- Confirm the
jobs.runnercolumn is"home":
SELECT slug, runner, state FROM jobs ORDER BY created_at DESC LIMIT 5;
- Watch the worker terminal — it should log a claim and begin generation.
(On the push daemon the equivalent log ispush_daemon.job_accepted
plus aPOST /jobs202 — the sweep pushes instead of the box polling.) - After the worker finishes,
stateshould be"paid"and Garage should
contain preview/master objects under the slug key namespace:
docker exec garage garage object list brioso-jobs
Running a single job via script (no GPU needed)
To land a runner="home" job without a GPU, for fast funnel testing:
RUNNER=home JOBS_ALLOW_RUNNER_OVERRIDE=true bun scripts/fake-complete.ts
This hits real HTTP endpoints so it exercises auth, schema validation,
idempotency, and completeGeneration — but skips the GPU entirely (track
keys point at objects that don’t exist, so audio players render empty).
Switching back to RunPod
Comment out or unset JOBS_DEFAULT_RUNNER. New jobs insert with
runner="runpod" and the dispatch sweep handles them. Restart the dev server.
Troubleshooting
Worker never claims jobs
- Is
jobs.runner = 'home'in the DB? Check withSELECT runner FROM jobs WHERE slug = '[[ORCA_RICH_MD:907aa2a23f6294e8b43528c898725d33:inline-html:%3Cslug%3E]]'. - Is
WORKER_SHARED_SECRETthe same in both web app and worker env? - Is the worker pointing at the right
JOBSTORE_API_URL?
Garage uploads return 403
- Did you run
garage bucket allowfor the key you created? - Is
R2_FORCE_PATH_STYLE=trueset in the web app env? - Is
R2_ENDPOINT=http://localhost:3900(not the real R2 endpoint)?
MLX weights download / venv bootstrap on first run
- The worker auto-bootstraps the Stable Audio 3 MLX repo on first use:
ensure_mlx_repo_cloned()clones the repo (if missing) and runs its
optimized/mlx/install.sh -y(if.venv/bin/pythonis missing). This is
non-interactive and can take a few minutes (dependency install + optional uv
install); the first generation job may appear stalled while it runs. - Set
HF_TOKEN=[[ORCA_RICH_MD:907aa2a23f6294e8b43528c898725d33:inline-html:%3Cyour%20HuggingFace%20token%3E]]in the worker’s env —sa3_mlx.pylazy-fetches the ~4 GB model weights from HuggingFace on first use after the venv exists. - If bootstrap fails (network, disk, permissions), the job fails and retries on the next lease cycle (5 min); fix the underlying error rather than re-running
install.shby hand.