fal-studio-auth-s3

/home/avalon/.hermes/skills/.archive/software-development/fal-studio-auth-s3/SKILL.md · raw

fal-studio auth + S3 migration

Use this when converting a simple single-user Vite + Express media app into a multi-user app with: - signup/login - cookie sessions - per-user third-party API keys - DB-backed gallery - S3-backed persistent media storage

This pattern was used for fal-studio.

Frontend: - Vite + React single-page app - tabs for Create / Gallery / Account - auth screen before app access - account tab stores the user's own fal key and shows balance

Backend: - Express single-port server - SQLite via better-sqlite3 - password hashing via bcryptjs - session cookie via jsonwebtoken + cookie-parser - S3 persistence via AWS SDK v3 against Hetzner Object Storage

Dependencies

npm install better-sqlite3 bcryptjs jsonwebtoken cookie-parser @aws-sdk/client-s3 dotenv

DB schema

Create at minimum:

users

assets

Auth pattern

Signup

  1. validate email/password
  2. hash password with bcrypt
  3. insert user row
  4. sign session JWT
  5. set secure httpOnly cookie
  6. return safe user object only

Login

  1. lookup by email
  2. compare password with bcrypt
  3. sign session JWT
  4. set cookie
  5. return safe user object only

Protected routes

Use middleware that: - reads cookie - verifies JWT - loads current user from DB - attaches req.user

Safe user shape

Never return full DB row. Return only:

{
  id,
  email,
  hasFalKey: !!fal_key,
}

Per-user provider key pattern

For fal-studio, do NOT use a global key in app state. Store each user's fal key in the users table and always use: - req.user.fal_key for generation, model access checks, and credit lookup.

Media persistence pattern

Before

After

S3 key pattern

Use user-scoped object keys such as: - users/{userId}/image/{timestamp}-{slug}.png - users/{userId}/video/{timestamp}-{slug}.mp4

This prevents cross-user collisions and makes cleanup simple.

fal generation flow

  1. require authenticated user
  2. require req.user.fal_key
  3. upload local reference images to fal CDN using that key
  4. call fal.run/{model} using that key
  5. detect result image vs video from response
  6. download provider output
  7. upload final media to Hetzner S3
  8. create DB asset row
  9. return S3 URL to frontend

fal billing / credits pattern

Expose an authenticated endpoint such as: - GET /api/me/fal-credits

Important experiential findings: - The working documented billing endpoint here was: - GET https://api.fal.ai/v1/account/billing?expand=credits - Earlier rest.alpha.fal.ai/... and rest.fal.ai/... billing attempts returned 404. - Do not assume one fixed billing URL will work forever; fallback logic is still reasonable, but prefer the documented api.fal.ai/v1/... route first. - Normalize both data.credits and data.billing.credits response shapes. - Normalize both credits.balance and credits.current_balance because the live response here used current_balance.

Also important: - Billing/credit lookup may require an ADMIN fal key even when the same key type can still run model inference. - If the billing endpoint returns 403 mentioning ADMIN keys, surface a user-friendly message like: - fal credit checks require an ADMIN fal API key. Your current key can run models but cannot read billing balance. - A 401 auth error across many different fal inference endpoints still usually means a bad key, not model activation.

Frontend account tab

Recommended controls: - current user email - whether fal key is saved - input to save/update fal key - credit balance card - refresh credits button - logout button

Frontend auth UX improvements worth keeping

Testing approach used

Write lightweight Node tests first for reusable modules: - auth helpers - DB helpers - storage key normalization - billing/credit helper - model upload section logic

Run with:

node --test auth-db.test.mjs fal-credits.test.mjs model-utils.test.mjs
npm run build

Deployment checklist

  1. add env vars for session secret + Hetzner S3
  2. build app
  3. restart PM2 with --update-env
  4. verify: - /api/status - signup/login - save fal key - gallery route - existing unrelated apps still return 200

Pitfalls