Closes the two gaps found smoke-testing the local stack: an empty gallery (zero media_files rows, media URLs hardcoded to the remote r2.dev bucket) and generation flows that could not complete (omni_gen calls providers synchronously in the handler, so dummy keys meant a 500 after billing; the webapp blocked submits earlier anyway because signup grants no wallet). Backend (all development-only): - CDN_BASE_URL env override in cdn_link.rs (host-only, validated loudly; precedence over the compiled-in hosts for every domain/env) and a dev-gated actix-files mount serving LOCAL_MEDIA_ROOT/media at /media, mirroring the public bucket layout - media URLs in API responses resolve against the backend itself - DEV_FAKE_GENERATION flag on ServerState (easyenv bool, forced off outside Development): the omni_gen image/video pipelines skip the external provider call and return a synthetic Fal enqueue (external id fake_<uuid>), so billing, prompt insert, and the pending generic_inference_jobs row all stay real - dev_fake_generation_resolver_thread: polls pending fake_ jobs (new runtime-query list_pending_fake_generation_jobs in mysql_queries) and after DEV_FAKE_GENERATION_RESOLVE_SECS (default 6s) marks them complete_success with a media_files row backed by a test_data asset copied under LOCAL_MEDIA_ROOT (mp4 results get -thumb.jpg/-thumb.gif siblings since the thumbnail job never runs locally), or complete_failure via the existing simulate_artcraft_failure prompt triggers (category word supported, e.g. "simulate_artcraft_failure face_not_detected") Bootstrap/seeds (PS + bash kept in lockstep): - seed_demo_user seeds an artcraft wallet (DEMO_CREDITS, default 100k banked + ledger rows; idempotent top-up) - signup creates no wallet and the webapp gates generate on balance, which is why submits went nowhere - seed_demo_user also seeds 6 gallery images (test_data jpgs) with objects under .devstack/media in the exact bucket fan-out layout - fixed the already-exists detection in seed_demo_user: create_account returns error_type, not error_code, so re-runs died on UsernameTaken - generated secrets env gains DEV_FAKE_GENERATION/CDN_BASE_URL/ LOCAL_MEDIA_ROOT (appended to existing files on bootstrap re-runs); stale "providers are only contacted by workers" comments corrected - doctor checks wallet balance, media rows, and the new secrets keys Docs: dev_setup_local_stack.md documents the new flags, corrects the sync-provider-call and upload-500 claims, updates the seams table and verification checklist; storyteller_web CLAUDE.md video model examples fixed (seedance_2p0_fast_g -> seedance_2p0_fast). Verified E2E on this machine: gallery lists 8 items on localhost URLs; image and video submits go pending ~6s then complete with locally-served results (mp4 + gif preview 200); failure trigger yields complete_failure with category face_not_detected; credits genuinely deducted (100000 -> 99932); doctor 0 failures / 0 warnings.
18 KiB
Local Dev Stack — Requirements & Bootstrap
How to run the full ArtCraft dev stack entirely locally — the
storyteller-web backend plus the artcraft-webapp frontend — with no traffic
to production servers. Scope: webapp-first; the Tauri desktop app is a later
phase.
The automated path on Windows (primary platform, native — no WSL, no Docker, no admin rights for the stack itself):
.\script\bootstrap\windows\bootstrap_dev_stack.ps1 # one-time setup, idempotent, re-run anytime
.\script\bootstrap\windows\run_backend_dev.ps1 # terminal 1: API on http://localhost:12345
.\script\bootstrap\windows\seed_demo_user.ps1 # terminal 2: create demo login (localdev1 / localdev1pass)
cd frontend; npx nx dev artcraft-webapp # terminal 2: webapp on http://localhost:4201
.\script\bootstrap\windows\dev_stack_doctor.ps1 # health check, any time
.\script\bootstrap\windows\stop_dev_services.ps1 # stop MySQL/Redis when done
The equivalent on Linux (Ubuntu 22.04+ — native, WSL2, or CI):
./script/bootstrap/bootstrap_dev_stack.sh # one-time setup, idempotent, re-run anytime
./script/bootstrap/run_backend_dev.sh # terminal 1: API on http://localhost:12345
./script/bootstrap/seed_demo_user.sh # terminal 2: create demo login (localdev1 / localdev1pass)
cd frontend && npx nx dev artcraft-webapp # terminal 2: webapp on http://localhost:4201
./script/bootstrap/dev_stack_doctor.sh # health check, any time
This document records the underlying requirements so the scripts aren't a black box, and corrects some stale claims in older docs.
Supported platforms
| Platform | Status |
|---|---|
| Windows 10/11 | Primary. script/bootstrap/windows/*.ps1 run a portable stack: MySQL 8.4 (official zip) and Redis live under the gitignored .devstack/ and run as plain user processes — no admin, no services, no WSL, no Docker. Build tools (cmake, perl, nasm, llvm) are offered via winget |
| Ubuntu 22.04+ | Supported by script/bootstrap/*.sh (native, WSL2, or CI runner/container); services via apt |
| macOS | Not covered by the scripts; follow _docs/dev_setup_server.md manually |
Windows specifics
- Everything stateful lives in
.devstack/(gitignored):mysql/(extracted zip),mysql-data/,redis/,logs/,pids/,downloads/(cached zips),my.ini. Delete the directory to fully reset; re-run bootstrap to rebuild. - Pinned services: MySQL 8.4.11 from
cdn.mysql.com(the 8.x series is required; only some patch versions exist on the CDN) and Redis 5.0.14.1 (tporadowski build — old but sufficient for the backend's keepalive/TTL/ rate-limit usage; overrideREDIS_ZIP_URLto use Memurai/Garnet instead). - The portable MySQL root user has no password (
--initialize-insecure) — local dev only, bound to 127.0.0.1. - diesel-cli is compiled against the portable MySQL's own client library
(
MYSQLCLIENT_LIB_DIR/MYSQLCLIENT_VERSION);libmysql.dllmust be onPATHto run it — the scripts handle both. - Compiling the backend needs cmake, Strawberry Perl, NASM, and LLVM/libclang
(the
wreqHTTP client builds BoringSSL, andaws-lc-sys/ringneed NASM/clang on MSVC). The bootstrap checks and offers winget installs. - The generated secrets file additionally sets
TEMP_DIR(the upload temp-dir helper defaults to/tmp, which doesn't exist on Windows).
Requirements matrix
| Requirement | Version / detail | Needed for |
|---|---|---|
| MySQL server | 8.x series (9.x unsupported by tooling) | Backend boot (eager connect) + migrations |
| Redis server | any recent | Backend boot (r2d2 pool connects eagerly) |
| Rust (stable) | no pinned MSRV; Docker builds use 1.93.0 | Building storyteller-web, diesel_cli |
| diesel_cli | cargo install diesel_cli --no-default-features --features mysql,sqlite |
Running the 281 migrations |
| Node.js | 20+ (enforced by script/common/frontend_preflight.sh) |
Frontend (Nx 21 / Vite 6) |
| npm | ships with Node; pnpm must not be used | Frontend install (frontend/package-lock.json) |
| System libs (apt) | build-essential, cmake, libssl-dev, libclang-dev, libfontconfig1-dev, fontconfig, perl, pkg-config, ffmpeg, libmysqlclient-dev, libsqlite3-dev, curl, jq | Compiling the workspace + diesel_cli; seeding scripts |
| Elasticsearch | optional (7.x) | Only 4 search endpoints; server boots without it |
| Docker | not required — no docker-compose exists | — |
Database identity (used consistently by docs, .env, and defaults):
database storyteller, user storyteller, password password, host
localhost.
Backend: what storyteller-web needs to boot
Binary: storyteller-web in crates/service/web/storyteller_web
(binaries: storyteller-web, docs — Swagger UI on :8989, docs-cli — emits
api.json). Binds 0.0.0.0:12345 (BIND_ADDRESS), plain HTTP.
Run it from the repo root — the config search path and includes/ paths
are cwd-relative.
Config loading (corrected facts)
- Mode is selected by the
SERVER_ENVIRONMENTprocess env var only (default: Development). Setting it inside an env file does nothing — it is read before any file loads. - In Development mode, files load from
crates/service/web/storyteller_web/config/in this order:storyteller-web.common.env→storyteller-web.development.env→storyteller-web.development-secrets.env(gitignored; the bootstrap generates it). - Precedence is first-value-wins (dotenv never overrides an existing key):
real process env > common.env > development.env > development-secrets.env.
The secrets file therefore cannot override anything already set in
development.env. - The repo-root
.envis NOT loaded by the server (it passesignore_legacy_dot_env_file: true). Root.env'sDATABASE_URLfeeds diesel-cli/sqlx-cli only; the server readsMYSQL_URL(defaultmysql://storyteller:password@localhost/storyteller).
Hard requirements at boot
| Requirement | Behavior if missing |
|---|---|
MySQL reachable via MYSQL_URL |
Boot fails (sqlx pool connects eagerly; the DB must exist and be migrated — health-check and ban-poller threads query immediately) |
Redis reachable via REDIS_0_URL (default redis://localhost/0) |
Boot fails (r2d2 pool connects eagerly) |
~30 get_env_string_required vars present |
Boot fails on the first absent var |
Of the required-present vars, 19 are supplied by no checked-in file and
live in the generated secrets file: ACCESS_KEY, SECRET_KEY, REGION_NAME,
W2L_PRIVATE_DOWNLOAD_BUCKET_NAME, W2L_PUBLIC_DOWNLOAD_BUCKET_NAME,
RESEND_API_KEY, FAL_API_KEY, GMICLOUD_API_KEY, GROK_API_KEY,
BEEBLE_API_KEY, OPENAI_API_KEY, WORLDLABS_API_KEY,
SEEDANCE2PRO_COOKIES, SEEDANCE2PRO_WHITELIST_COOKIES,
SEEDANCE2PRO_BYTEPLUS_ULTRA_COOKIES, STRIPE_FAKEYOU_ACCOUNT_ID,
STRIPE_ARTCRAFT_ACCOUNT_ID, STRIPE_ARTCRAFT_SECRET_KEY,
STRIPE_ARTCRAFT_SECRET_WEBHOOK_KEY.
Placeholder values are safe at boot: none of these services is contacted at boot (clients are constructed offline; Elasticsearch, S3/R2, Stripe, Resend, and Google-cert fetches are all lazy). But two earlier claims here were wrong and are corrected:
- The omni_gen endpoints call providers synchronously in the HTTP handler
(
/v1/omni_gen/generate/{image,video}— the path the webapp uses). With dummy keys the provider call fails and the submit returns HTTP 500 after billing the wallet (the image pipeline has no refund; video refunds only Kinovi). Only the legacy polling providers (GmiCloud, Grok, Kinovi orders, WorldLabs) are worker-driven.DEV_FAKE_GENERATION(below) exists so local submits never reach a real provider. - Uploads fail hard with dummy S3 creds: every media upload endpoint
uploads to the bucket before inserting the DB row, and
rust-s3is built withfail-on-err— so the first upload returns HTTP 500. (Known seam; uploads are not yet part of the local stack.)
Fully-local generation and media (DEV_FAKE_GENERATION)
The generated secrets file enables three development-only settings (appended automatically on bootstrap re-runs for older stacks):
| Env var | Effect |
|---|---|
DEV_FAKE_GENERATION |
omni_gen image/video submits skip the external provider and insert a normal pending job with a fake_… external id. A resolver thread inside storyteller-web completes each one after DEV_FAKE_GENERATION_RESOLVE_SECS (default 6s, poll every DEV_FAKE_GENERATION_POLL_SECS, default 2s) with a placeholder result copied from test_data/. Prompts containing simulate_artcraft_failure (or test_artcraft_failure) fail instead, optionally with a FrontendFailureCategory word in the prompt (e.g. simulate_artcraft_failure face_not_detected). Hard-disabled outside SERVER_ENVIRONMENT=Development |
CDN_BASE_URL |
Overrides the compiled-in CDN hosts in cdn_link.rs for all media URLs in API responses. Must be scheme+host only (no path) — set to http://localhost:12345 so media resolves against the backend itself |
LOCAL_MEDIA_ROOT |
Directory the backend serves at /media (dev-only actix-files mount), mirroring the public bucket layout: object path /media/{…} lives at {LOCAL_MEDIA_ROOT}/media/{…}. The bootstrap points it at .devstack/media/ |
With all three set, the full loop works offline: submit → pending job →
polling (/v1/jobs/session) → success with a media_files row whose URL the
backend itself serves — or a failed job with a real failure category. Credits
are genuinely deducted from the seeded wallet (no refund on fake results —
the demo wallet is large enough not to care).
Auth on localhost
POST /v1/logintakes{"username_or_email": "...", "password": "..."}; success returns{"success": true, "signed_session": "<jwt>"}and asessioncookie.- With the checked-in dev config (
COOKIE_DOMAIN=.jungle.horse), the session cookie is issued non-Secure and host-only (the Domain attribute is never set), so plain-HTTP localhost login works as-is. The frontend additionally storessigned_sessionin localStorage and sends it as asessionheader. - Dev-mode CORS allows any
localhost/127.0.0.1origin on any port, with credentials. - Health endpoint for scripts/CI:
GET /_status.
Build
- Compile with
SQLX_OFFLINE=true(uses the checked-in.sqlx/cache; 483 queries). Without it, the sqlx macros open a live DB connection at compile time usingDATABASE_URLfrom the root.env.SQLX_OFFLINEhas no runtime effect — ignore older notes saying not to set it when running. - First build takes several minutes; the crate has ~370 pre-existing warnings.
Database: migrations and seed data
- 281 Diesel migrations in
_database/sql/migrations/(diesel.tomlat repo root points there). Run from the repo root:diesel migration run."Encountered unknown type for Mysql: enum"warnings are harmless.migrations_squashed/is documentation only — not executable. - Seed data:
_database/sql/seed/sql/system_roles.sql(rolesuser,mod,admin) anduser_badges.sql. Theuserrole is mandatory — account creation hardcodesuser_role_slug='user'. The legacy wrapperbootstrap_inserts_roles_etc.shmust run with cwd_database/sql/and is not idempotent; the bootstrap script applies the same SQL files with idempotency guards instead. - Demo user: created through
POST /v1/create_account(no captcha, no invite code, no email-verification gate; password just needs 6+ chars). This exercises the real signup path instead of hand-rolled SQL. Gotcha: usernames likedemo,admin,test,dev,artcraftare reserved (includes/binary_includes/usernames/reserved_usernames.txt) — the default demo user islocaldev1. The Rust seeder (cargo run --bin dev-database-seed) exists but hard-requires a.env-secretsfile and also seeds model weights, so the HTTP path is preferred. - Demo credits: signup creates no wallet, and the webapp gates the
generate button on wallet balance — so
seed_demo_useralso seeds anartcraft-namespace wallet withDEMO_CREDITS(default 100,000) banked credits, plus matching ledger rows. Idempotent: re-runs top the balance back up toDEMO_CREDITSbut never lower it. (Raw SQL, because the only real credit-granting paths are Stripe webhooks and moderator tooling.) - Demo gallery:
seed_demo_useralso seeds a handful ofmedia_filesimage rows (fromtest_data/image/) with their objects copied under.devstack/media/, so the library isn't empty on a fresh stack. Rows mirror what a real image upload writes (media_class='image'matters — the library filters on it, and the column default'unknown'is invisible in every tab).
Frontend: artcraft-webapp
- Nx workspace at repo-root
frontend/(npm workspaces;npm installfromfrontend/). The FontAwesome Pro registry token is committed infrontend/.npmrc— no manual auth step. - Serve:
npx nx dev artcraft-webapp→http://localhost:4201. No lib pre-build is needed in dev (libs resolve to TS source via the"development"export condition); productionnx builddoes need lib builds. - In dev the webapp already targets the local backend:
src/main.tsxcallsStorytellerApiHostStore.setDevelopment()underimport.meta.env.DEV, pointing all API traffic athttp://localhost:12345. (The adjacent comment claiming this is disabled is stale — the call is live, and it bypasses the Vite/v1→api.storyteller.aiproxy.) So: local backend running → fully local stack; local backend not running → dev webapp is broken, not silently on prod. - Optional:
VITE_GOOGLE_CLIENT_IDinfrontend/apps/artcraft-webapp/.envenables the Google SSO button; email/password login needs nothing. - Other dev ports if you run more apps: website 4200, dashboard 4202 (separate pnpm app, excluded from Nx), video-info-website 4250, genhub 4300, Tauri frontend 5173.
What still touches production (known, deliberate seams)
Cleaning these up is the next phase of the infra work; the bootstrap does not change runtime behavior:
| Seam | Today |
|---|---|
| Media/CDN URLs in API responses | Closed when CDN_BASE_URL + LOCAL_MEDIA_ROOT are set (the bootstrap default): media URLs point at the backend's own /media mount. Unset, the compiled-in cdn_link.rs constants still point at the public pub-….r2.dev bucket |
| Media uploads | Upload endpoints push to R2/S3 before the DB insert and rust-s3 is fail-on-err — with dummy creds every upload endpoint returns HTTP 500. A local S3 (S3_COMPATIBLE_ENDPOINT_URL → MinIO) or a filesystem storage mode is the missing piece |
Vite /v1 proxy in webapp/website |
Hardcoded https://api.storyteller.ai (bypassed in dev by setDevelopment(), but still the fallback path) |
SegmentationApi, GetCdnOrigin, API_TARGETS |
Hardcoded off-store production hosts (GetCdnOrigin/Api.ts still hardcode cdn-2.fakeyou.com for pagescene/moodboard surfaces, independent of the backend's CDN_BASE_URL) |
| Elasticsearch-backed search | search_featured / search_session media files, weights/search, legacy tts/search fail without a local ES + elasticsearch-cli reindex (optional) |
CI usage
The same bootstrap works on a fresh Ubuntu runner/container:
export MYSQL_ROOT_PASSWORD=root # GitHub Actions' preinstalled MySQL uses password auth
./script/bootstrap/bootstrap_dev_stack.sh --yes
./script/bootstrap/run_backend_dev.sh &
./script/bootstrap/seed_demo_user.sh # waits for /_status
./script/bootstrap/dev_stack_doctor.sh
--yes answers all prompts; --skip-frontend / --skip-rust-build /
--skip-packages slice the work for focused jobs.
Verification checklist
./script/bootstrap/dev_stack_doctor.shexits 0 with no FAILs.POST /v1/loginwith the demo user returnssuccess: trueplus asigned_session(done automatically byseed_demo_user.sh).- Webapp at
http://localhost:4201can log in aslocaldev1, has credits in the top bar, and sees the seeded images in the library. - A generation submit returns
{"success": true, "inference_job_token": …}, shows as pending for a few seconds, then completes with a placeholder result (DEV_FAKE_GENERATION) — no external provider is contacted. A prompt containingsimulate_artcraft_failurefails with a real error toast/category instead. - Nothing hits
api.storyteller.aior the r2.dev CDN during the above (verify in the browser network tab — media URLs should point athttp://localhost:12345/media/…).
Corrections to older docs
crates/service/web/storyteller_web/CLAUDE.md: the repo-root.envis not auto-loaded by the server, andSQLX_OFFLINE=trueis fine (and recommended) when running — it only affects compile-time query checking._docs/dev_setup_server.md:./script/dev_mysql_connect.shdoesn't exist — it's./script/dev_database_connect.sh.diesel.toml's comment about needing an unreleased diesel CLI is stale — any currentdiesel_cliworks.TESTING_INFRA_HANDOFF.md(untracked): the server env var isMYSQL_URL, notDATABASE_URL; migration count is 281; crate path iscrates/service/web/storyteller_web.