Brings up a fully local storyteller-web: portable MySQL 8.4 and Redis under a gitignored .devstack/ (plain user processes -- no admin, no services, no WSL, no Docker), all 281 migrations, role/badge seeds, and a demo login with credits. Windows and Linux variants, plus a doctor that reports what is and isn't healthy. Scripts and docs only. Nothing here is compiled into any binary, and no application behaviour changes: the backend it starts is the one that ships. Integrations that cannot be provided locally -- object storage, generation providers, email -- get placeholder credentials, so those endpoints fail when exercised. Swap in a real dev credential for the one you are working on. The doc records exactly which areas that leaves working. An earlier revision closed those gaps with development-only behaviour inside storyteller-web (DEV_FAKE_GENERATION, a filesystem bucket mode, a CDN_BASE_URL override). That is deliberately not here: a switch that exists in the shipped binary can be flipped. Frontend work that wants a populated, working stack should use frontend/apps/fake-storyteller-web instead.
20 KiB
Local Dev Stack — Requirements & Bootstrap
How to run storyteller-web on your own machine against a local MySQL and
Redis — no shared dev database, no Docker, no admin rights. This is for work
on the backend itself.
Working on the frontend? You probably want
frontend/apps/fake-storyteller-webinstead — an in-memory stand-in for this API that needs no database, no build, and no setup. Everything below is unnecessary for webapp work. Come back here when you need the real server.
These scripts install and supervise infrastructure. They change no application behaviour, and nothing in them is compiled into any binary: the backend they start is the same one that ships. Integrations they cannot provide locally (object storage, generation providers, email) get placeholder credentials, so the endpoints that use them fail when exercised. Swap in a real dev credential for whichever one you're actually working on.
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)
$env:VITE_DEV_API_HOST = "http://localhost:12345" # terminal 2: aim the webapp at your backend
cd frontend; npx nx dev artcraft-webapp # 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 && VITE_DEV_API_HOST=http://localhost:12345 npx nx dev artcraft-webapp # terminal 2: webapp on :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 placeholder 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. - Uploads fail hard with placeholder S3 creds — every media upload endpoint
uploads to the bucket before inserting the DB row, and
rust-s3is built withfail-on-err.
What does and doesn't work locally
| Area | Works with placeholder credentials? |
|---|---|
| Login, sessions, account, profile | Yes |
| Wallets, credits, billing reads | Yes |
| Folders, tags, prompts, characters, projects | Yes |
| Media listing (rows) | Yes — but the images 404, see below |
| Media uploads | No — needs real R2/S3 credentials |
| Image/video/mesh/splat generation | No — needs a real provider key |
| Transactional email | No — needs a real Resend key |
| Elasticsearch-backed search | No — needs a local ES + reindex |
The rows-but-no-images case is the one that surprises people: media_files
rows carry a bucket path, and the URL the API builds from it points at the
shared R2 bucket, which does not have your local objects. Put real dev R2
credentials in the secrets file if you need media to render, or use
fake-storyteller-web, which serves its own bytes.
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: not seeded. Media rows without reachable bytes render as
broken tiles, so seeding them locally would be worse than an empty library.
Use
fake-storyteller-webfor a populated one.
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. - By default the dev webapp targets production.
src/main.tsxpoints the API host at the Vite dev origin, and Vite proxies/v1tohttps://api.storyteller.ai. The adjacentsetDevelopment()call — which would point it atlocalhost:12345— is commented out. To aim the webapp at your local backend, setVITE_DEV_API_HOST=http://localhost:12345beforenx dev(that override ships with thefake-storyteller-webchange; until it lands, uncommentsetDevelopment()locally instead). - 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)
The bootstrap changes no runtime behaviour, so these remain open. Each is closed by supplying a real credential, not by a code path:
| Seam | Today |
|---|---|
| Media/CDN URLs in API responses | The compiled-in cdn_link.rs constants point at the public pub-….r2.dev bucket, so a locally-created row's media URL resolves to an object that isn't there |
| Media uploads | 500 with placeholder credentials; supply real dev R2/S3 keys to exercise them |
| Generation | 500 with placeholder keys, after the wallet is billed; supply a real key for the provider under test |
Vite /v1 proxy in webapp |
Targets https://api.storyteller.ai, so a dev webapp with no host override reads and writes production |
| 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).GET /_statusreturns 200, and MySQL has the full migration set applied.- With
VITE_DEV_API_HOSTpointed atlocalhost:12345, the webapp logs in aslocaldev1and shows its credits in the top bar. The library is empty and generation 500s — both expected, see the table above.
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.