--- 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 billing endpoint that worked here was `GET https://api.fal.ai/v1/account/billing?expand=credits` - Billing reads may require an ADMIN key even when inference works with a regular 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` ## 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 - [ ] 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