--- name: fal-studio-app-patterns description: Class-level patterns for building and evolving fal-studio-style Vite+Express media apps with auth, per-user provider keys, schema-driven model inputs, concurrent jobs, gallery persistence, and shareable outputs. version: 1.0.0 author: Hermes Agent license: MIT metadata: hermes: tags: [fal-studio, fal.ai, vite, express, media-apps, auth, gallery, concurrency, schema-driven-ui] related_skills: [hetzner-s3-storage, vps-app-deployment, multi-provider-api-resilience] --- # fal-studio App Patterns ## Overview Use this for fal-studio-class apps: Vite + React frontend, Express backend, user-supplied model/provider keys, generated image/video outputs, persisted galleries, and a fast-moving model catalog. This umbrella replaces narrower one-session skills. The right abstraction is not "auth migration" or "dynamic uploads" alone, but the broader app class: a multi-user media-generation studio whose UI and storage model must stay resilient as providers, models, and asset flows change. ## When to Use - Adding auth, persistence, or user-scoped provider keys to a simple generator app - Supporting many fal.ai image/video endpoints with different upload requirements - Letting users launch multiple jobs while switching models on mobile - Persisting generated outputs to local disk or object storage with durable gallery/share links - Normalizing provider errors and temporary preview URLs into reliable app behavior ## Core architecture - Frontend: Vite + React SPA with Create / Gallery / Account flows - Backend: Express single-port app - Persistence: SQLite for users/assets metadata - Storage: durable local files or S3-compatible object storage - Provider access: per-user API key stored on the user record, not global app state ## 1. Auth + persistence + storage migration When converting a single-user prototype into a real app: - add signup/login - store per-user fal keys - persist gallery rows in SQLite - upload final outputs immediately to durable storage - prefer saved asset URLs over provider preview URLs everywhere user-visible Recommended tables: - `users(id, email, password_hash, fal_key, created_at)` - `assets(id, user_id, model, prompt, result_kind, storage_key, storage_url, params_json, created_at)` Safe user shape: ```js { id, email, hasFalKey: !!fal_key } ``` ## 2. Schema-driven model input UI Do not hardcode model assumptions from memory. For each endpoint: 1. inspect the real OpenAPI schema 2. derive required fields, enums, defaults, upload requirements, and output kind from that schema 3. use human docs only as a supplement for descriptions/pricing/prompt hints Represent uploads by semantic section keys, not one generic image input: - `generalReferences` - `firstFrame` - `lastFrame` - `storyboardReference` Backend should use `multer.any()` and group files by field name before mapping them to API params. ## 3. Concurrency and model-switch resilience A single global `generating` boolean is too weak for model-comparison workflows. Use a lightweight frontend job queue: - `queued` - `running` - `completed` - `failed` Preserve only compatible upload buckets across model switches. Users should not lose valid references just because they compare two nearby models. ## 4. Error normalization Provider errors often arrive as nested objects, arrays, moderation payloads, or opaque strings. Normalize in shared helpers on both backend and frontend: - unwrap `error`, `detail`, `message`, `msg`, `reason` - join validation arrays clearly - map status classes to user-facing categories: - `401` invalid/missing key - `403` safety/policy or restricted capability - `422` invalid params or blocked request - `429` rate limit - `5xx` provider-side failure ## 5. Durable gallery and share links Treat persisted assets as the source of truth. Rules: 1. save metadata at write time 2. return stable asset URLs from the backend 3. derive absolute share URLs server-side 4. gallery and result viewers should prefer persisted URLs over preview URLs 5. never rely on temporary provider URLs for long-lived gallery/share behavior Useful metadata to persist: - `result_kind` - `storage_mode` - `inference_time` - `output_duration` - request params/model name/prompt ## 6. fal-specific operational lessons - The official balance endpoint is `GET https://api.fal.ai/v1/account/billing?expand=credits`; it returns the account username and current credit balance. - Billing reads require an ADMIN key even when inference works with a regular API key. A live `403` on billing can therefore mean "valid generation key, insufficient admin scope," not a bad key. - Use separate `fal_admin` (account broker only) and `fal_runtime_api` (generation adapter only) credentials. From the ADMIN key, `POST /v1/keys` can mint the dedicated runtime key and returns its secret once. - Never mark fal connected merely because an encrypted credential write succeeded. Verify the exact workspace identity and balance, then confirm the binding; support an explicit degraded `generation_only` mode when the customer supplies only an API key. - A provider `401` across many endpoints usually means a bad user key, not a model activation issue. - For moderation/safety failures, surface explicit policy text instead of a generic `Generation failed`. ## 7. Marketing-only capabilities and upstream native routes A provider product page can advertise a workflow that its generic public wrapper schema does not expose. Do not force the advertised semantics through superficially similar fields. Seedream 5.0 Pro native layers are the concrete warning case (last verified 2026-07-23): - fal's `num_images` requests independent completed generations; it is not a semantic layer count. - Prompting fal's ordinary edit endpoint for layers while sending `num_images: 1` returned one flattened image in two observed completed requests, even though the prompt asked for transparent layers. - Do **not** substitute the public BytePlus ModelArk image endpoint. Its current capability table explicitly documents `seedream-5-0-pro` as **single-image generation**, says sequential generation is unsupported, and says passing `sequential_image_generation` causes an error. - The often-quoted 15-image combined input/output ceiling belongs to Seedream 5.0 Lite / 4.5 / 4.0 sequential generation, not Pro. It cannot be used as a Pro billing cap. - A top-level BytePlus `data[]` response shape does not prove that Pro can return multiple semantic layers; the documented Pro contract can return a one-item array. - Until an exact public layer endpoint, request field, provider-enforced output ceiling, and observed transparent-layer response are established, fail closed before upload or paid provider activity. Preserve the local compositor/demo separately. General rule: establish the exact endpoint, request switch, response fixture, billing ceiling, and output semantics from official docs or an observed provider response before implementing a marketing claim. A provider page or demo can advertise a private/orchestrated workflow that neither its wrapper nor upstream public API exposes. ## 8. Account bootstrap, funding, and assisted setup Keep fal out of zero-field product signup and request it just in time when paid media work needs it. Use one **Connect fal** entry for both new and existing users because fal itself combines login and signup through Google, GitHub, or SSO. The fastest supported direct flow is human OAuth/terms → select account context → paste one ADMIN setup key → broker verifies username/balance → broker creates an API-only runtime key. Agentcard Companies (`agentcard.sh`, not `agentcard.ai`) can provide embedded per-user connection, KYC, user-funded wallet, multi-use cards, and approvals. For browser-assisted fal funding, keep Hermes as planner/proposer while a restricted deterministic payment runner injects card details outside model context. Human turns remain OAuth identity, terms, CAPTCHA, KYC, 3DS, OTP, and exact spend approval. Obtain fal written authorization or a first-party partner provisioning path before production dashboard automation. See `references/fal-account-bootstrap-agentcard-onboarding.md` for exact provider facts, connector states, two-key design, Agentcard funding flow, restricted browser runner boundary, managed-fal alternative, source links, and pitfalls. ## Common Pitfalls 1. Using a global provider key in app state instead of the authenticated user's key 2. Modeling uploads as one `imageFile` instead of semantic upload buckets 3. Preferring `previewUrl` over saved URLs in gallery/share flows 4. Blocking the whole app with one global spinner when users want multiple jobs 5. Passing raw provider error objects to the UI and showing `[object Object]` 6. Forgetting `credentials: 'include'` for cookie-authenticated fetches ## Verification Checklist - [ ] Auth flow returns safe user objects only - [ ] Generation requires and uses the correct per-user provider key - [ ] fal ADMIN and runtime API capabilities are isolated; neither raw secret reaches model context or browser responses - [ ] fal readiness requires live workspace/balance verification, while API-only credentials are labeled `generation_only` - [ ] New and existing fal users share one just-in-time connector without blocking initial product signup - [ ] Any Agentcard/browser-assisted funding path preserves human terms/KYC/3DS turns and reconciles card transaction state against fal credits before retry - [ ] Multi-upload models map files to the right API fields - [ ] Users can launch multiple jobs and inspect prior results - [ ] Saved gallery items survive expired provider preview URLs - [ ] Share links are absolute and openable outside the app - [ ] Error messages are readable and category-aware