Files
briant 0f5f28b90d feat(packages): add @civitai/flipt and wire creator-studio to it
Extracts the monolith's Flipt client into a shared workspace package so other
apps can gate on the same flags. The package carries over the production
hardening as-is — init timeout, failure circuit breaker, generational TTL eval
cache, dev-only local overrides, fail-closed on every path — and follows the
@civitai/axiom / @civitai/redis shape: raw TS entry, package-owned lazy env,
injected logging.

Flag keys stay in the app. FLIPT_FEATURE_FLAGS and the cache-bypass set remain
in src/server/flipt/client.ts and are passed to the factory as `cacheBypass`;
which flags are incident kill-switches is app knowledge, not package knowledge.

Connection env resolves on the first evaluation rather than at construction, so
a missing FLIPT_URL degrades that instance to fail-closed instead of throwing
out of the import that built it. Building it eagerly broke 61 unit suites at
collection, whose env mock has no Flipt vars.

creator-studio gets a getFlipt() shim mirroring its redis.ts/logger.ts (lazy,
globalThis-cached, app logger injected), the transpile + dep entries, documented
env, and a test pinning the wiring — nothing else in the app imports the shim
yet, so without it neither svelte-check nor a build would prove it resolves.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 12:20:41 -06:00

84 lines
4.9 KiB
Bash

# Creator Studio app — env template. Copy to `.env` and fill in (real values live in the main app's repo-root .env).
#
# This app is the creator-studio.civitai.com SPOKE: it shares the hub's `.civitai.com` session cookie, so it needs NO
# login UI, NO OAuth-bridge endpoints, and NO TrustedSpokeDomain row (same registrable domain → shared cookie).
# The door gate is "any authenticated user"; member-only ACTIONS are gated per-action on the user's tier.
# --- Auth (spoke) ---
# The hub origin (token issuer). Used to verify the JWT `iss` and to build the login redirect.
AUTH_JWT_ISSUER=https://auth.civitai.com
# The hub's public keys, for LOCAL ES256 verification (no per-request hop for the signature check).
AUTH_JWKS_URI=https://auth.civitai.com/api/auth/jwks
# Shared service secret — lets the session client do INTERNAL-authed read-through against the hub.
AUTH_INTERNAL_TOKEN=
# Local dev against a LOCAL hub: point the two URLs at it, e.g.
# AUTH_JWT_ISSUER=http://localhost:5173
# AUTH_JWKS_URI=http://localhost:5173/api/auth/jwks
# (loopback hosts are auto-trusted by the hub in dev, so no seeding needed.)
# Where an unauthenticated user who can't be redirected to login ends up. Defaults to https://civitai.com.
# CIVITAI_APP_URL=https://civitai.com
# Shared main-app webhook token — required only to set permanent paid access (the main-app endpoint gates the
# `permanent` flag on it). Use the same value as the main app's WEBHOOK_TOKEN.
WEBHOOK_TOKEN=
# --- Database (Kysely read/write via @civitai/db/kysely, src/lib/server/db.ts) ---
# Point write at the primary and read at a replica (use the same URL for both if you have no replica).
# `sslmode=no-verify` is forced in code for the cnpg pooler's self-signed cert, so the URLs here don't need it.
DATABASE_URL=postgresql://user:pass@localhost:5432/civitai
DATABASE_REPLICA_URL=postgresql://user:pass@localhost:5432/civitai
# --- Axiom logging (@civitai/axiom, src/lib/server/logger.ts) ---
# All optional. Without AXIOM_TOKEN the logger is stderr-only, and it only ships to Axiom when
# NODE_ENV=production — so local dev needs none of these set.
AXIOM_TOKEN=
AXIOM_ORG_ID=
AXIOM_DATASTREAM=
# --- ClickHouse (analytics reads via @civitai/clickhouse, src/lib/server/clickhouse.ts) ---
# Powers /earnings/analytics (content metrics). Required in production; local dev can leave these blank —
# queries just fail and the page degrades gracefully.
CLICKHOUSE_HOST=https://clickhouse.example.com
CLICKHOUSE_USERNAME=default
CLICKHOUSE_PASSWORD=
# --- Redis (analytics read-through cache via @civitai/redis, src/lib/server/redis.ts + cache.ts) ---
# BOTH are required or `@civitai/redis` throws (caught as fail-open — the cache is then silently skipped and
# every read hits ClickHouse). Use the same URL for both if you have no separate system instance.
REDIS_URL=redis://:password@localhost:6379
REDIS_SYS_URL=redis://:password@localhost:6379
# --- Feature flags (@civitai/flipt, src/lib/server/flipt.ts) ---
# Both required to evaluate flags; without them every flag reads false (the client logs one init error and
# stays fail-closed), which is a fine local-dev default. Same values as the main app's .env.
FLIPT_URL=
FLIPT_FETCHER_SECRET=
# Flags live in the `civitai-app` Flipt environment alongside the main app's. Override to split them.
# FLIPT_ENVIRONMENT=civitai-app
# Dev-only: short-circuit flags without touching shared Flipt state, e.g. `my-flag=on,other-flag=variant`.
# FLIPT_LOCAL_OVERRIDES=
# --- Media CDN (EdgeImage, for later model/creator imagery) ---
# Cloudflare-images delivery base. PUBLIC_ prefix exposes it to the browser via $env/dynamic/public.
# Mirrors the main app's NEXT_PUBLIC_IMAGE_LOCATION (prod value below; point at your dev cacher for local).
PUBLIC_IMAGE_LOCATION=https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA
# --- Buzz service (@civitai/buzz, src/lib/server/buzz.ts) ---
# Base URL of the buzz microservice — the authoritative source for buzz-account balances, including Creator
# Program cash (cashSettled = ready-to-withdraw, cashPending). Read via the buzz service, NOT ClickHouse
# (buzzTransactions is a mirror + flow log that can drift from the real balance). Required in production; local
# dev can point at a local instance/tunnel (the main app uses :8080).
BUZZ_ENDPOINT=http://localhost:8080
# --- Creator Program pool (src/lib/server/creator-program.ts) ---
# The "your Buzz could be worth $X" estimate on /join replicates the main app's compensation-pool math. These are
# the same runtime secrets the main app reads (src/server/services/creator-program.service.ts). Fill from the main
# app's .env. TAXES = % taxes/fees on the gross pool input; PORTION = % of net that funds the pool value;
# FORECAST_PORTION = % of prior-month buzz used as the forecast base. If TAXES/PORTION are left blank the estimate
# degrades exactly as the main app does (poolValue falls back to 35000); set them to match the buzz dashboard.
CREATOR_POOL_TAXES=
CREATOR_POOL_PORTION=
CREATOR_POOL_FORECAST_PORTION=50