Skip to main content

Installation

Set up a local Lumio development environment.

Hosted Environments

Lumio runs three environments per app. Production lives on lumio.vision; non-production on zaflun.dev. Use these strings when configuring OAuth callbacks, CORS, env-var defaults and CI deploy targets.

AppProductionProduction PreviewStaging
Webapplumio.visionlumio.web.prod.zaflun.devlumio.web.staging.zaflun.dev
ID Appid.lumio.visionlumio.id.prod.zaflun.devlumio.id.staging.zaflun.dev
Admin Appadmin.lumio.visionlumio.admin.prod.zaflun.devlumio.admin.staging.zaflun.dev
Stats Appstats.lumio.visionlumio.stats.prod.zaflun.devlumio.stats.staging.zaflun.dev
APIapi.lumio.visionlumio.api.prod.zaflun.devlumio.api.staging.zaflun.dev
Docsdocs.lumio.visionlumio.docs.prod.zaflun.devlumio.docs.staging.zaflun.dev
Developer Docsdevelopers.lumio.visionlumio.developer-docs.prod.zaflun.devlumio.developer-docs.staging.zaflun.dev
Overlay external URLoverlay.lumio.vision(single per-deployment external host; configurable via NEXT_PUBLIC_OVERLAY_URL on the webapp)(same)
Widget external URLwidget.lumio.vision(single per-deployment external host; configurable via LUMIO__WIDGET__PUBLIC_URL on the API)(same)

Branch mapping: next → staging · main → production preview · tag 20* → production. This mapping is enforced in CI for the Cloudflare-hosted docs family (docs, developer-docs, extension-apps). The core product containers (API, web, admin, id, workers, bots) are container images promoted by release channel instead — beta for a pre-release, latest for a full release.

The rest of this guide covers a local development setup.

Prerequisites

ToolVersion / notes
Docker + Docker ComposeRuns the local database stack
Rust toolchain (rustup)Edition 2024. CI builds on stable; the Bazel toolchain pins 1.97.1
protocRequired — lo-youtube-api's build script compiles the YouTube gRPC protos
Node.js>= 18
pnpm10.28.0 (see packageManager in the root package.json)
justTask runner used by every command in these docs

Bazel is optional locally — it is the CI build system (see Bazel below).

Dev Stack

just stack-up starts the local database + broker stack, merging dev-stack/db.docker-compose.yml and dev-stack/broker.docker-compose.yml:

just stack-up
ServiceImageHost portDatabase
lumio-postgrespostgres:18-alpine127.0.0.1:5432lumio (user/password lumio)
lumio-tsdbtimescale/timescaledb-ha:pg18127.0.0.1:5433lumio_tsdb (user/password lumio)
lumio-extensions-postgrespostgres:18-alpine127.0.0.1:5434lumio_extensions (user/password lumio_ext)
lumio-redisredis:8-alpine127.0.0.1:6379password lumio
lumio-rabbitmqrabbitmq:4-management-alpine127.0.0.1:5672 (AMQP), 127.0.0.1:15672 (UI)user/password lumio
lumio-kafkaapache/kafka:3.9.0127.0.0.1:9092KRaft mode (no ZooKeeper)

The brokers back the ingest pipeline: RabbitMQ is the job/command bus and Kafka is the durable ingest firehose (Redis stays cache-only, TimescaleDB is the system of record). Dev and staging run single-node; only production is the multi-node cluster.

The stack runs in the foreground, so use a dedicated terminal. Related recipes:

RecipeWhat it does
just stack-downStop the stack (volumes kept)
just stack-resetStop the stack, drop the volumes, start again
just stack-logsTail the stack logs
just mail-upMailDev on http://localhost:1080
just minio-upMinIO console on http://localhost:9001
just stack-allstack-up + mail-up + minio-up
just psql / just tsdb-psql / just redis-cliOpen a CLI against the running containers
just rabbitmq-uiPrint the RabbitMQ management UI URL (http://127.0.0.1:15672, lumio/lumio)
just kafka-topicsList Kafka topics on the local KRaft broker

Database Migrations

Migrations live in apps/api/migrations/ (main database) and apps/api/tsdb_migrations/ (TimescaleDB) and are run by the migrate binary:

just migrate # run all pending migrations on both databases
just migrate-status # show what has been applied
just migrate-main # main database only
just migrate-tsdb # TimescaleDB only
just db-fresh # drop, create, migrate

The API also runs pending migrations on startup.

Migrations are embedded at compile time

sqlx::migrate!() embeds the .sql files into the binary when it is compiled. Adding a new migration file therefore requires a rebuild — cargo build -p lumio-api (or just build-api) — not just a restart. A restart alone will silently run the old migration set.

New migrations use the reversible .up.sql / .down.sql pair format. Legacy migrations from before 20260410 use the simple single-file format and are never renamed.

Running the Apps

Each app has its own recipe and its own port:

RecipeAppURL
just run-api (or just dev-api for auto-reload, which needs cargo install cargo-watch)Rust APIhttp://localhost:3000
just dev-webWebapp / dashboardhttp://localhost:4000
just dev-adminAdmin panelhttp://localhost:4001
just dev-idID / auth apphttp://localhost:4002
just dev-statsPublic stats apphttp://localhost:4003
just dev-docsThis documentation siteDocusaurus default port — pass --port to avoid clashing with the API on 3000
just dev-developer-docsDeveloper documentationhttp://localhost:3004
just dev-supervisorExtension supervisor (static)http://localhost:3200
just dev-runtimeExtension runtime (static)http://localhost:3201

The bots have matching recipes: just run-twitch-bot, just run-youtube-bot, just run-kick-bot, just run-trovo-bot, just run-discord-bot (and a dev-* variant of each).

Bazel (CI Only)

Bazel handles CI builds and container image creation. It is not required for local development -- Cargo covers that. Install Bazelisk, which automatically downloads the correct Bazel version:

# Install Bazelisk (Bazel version manager)
npm install -g @bazel/bazelisk
# Or on macOS:
brew install bazelisk

# Verify
bazel --version

See the Bazel Build System guide for details on the build architecture, vendored crates, and image targets.

Environment Configuration

Configuration is split by runtime. There is no repository-root .env — each app is configured on its own.

Rust services — layered TOML

The API and the bots read layered TOML from their own config/ directory:

config/default.toml → config/{env}.toml → config/local.toml → ENV vars

Later layers win, so an environment variable always overrides a file. config/default.toml is committed and holds every key with a documented default; config/local.toml is git-ignored and is where you put local secrets. The {env} layer is selected by APP_ENV for the API (development — the default — staging, or production), and by {SERVICE}_RUN_MODE for the bots (e.g. TWITCH_BOT_RUN_MODE).

Create apps/api/config/local.toml with at least your OAuth login credentials and a token encryption key (see OAuth Provider Setup). The database and Redis defaults in default.toml already match the dev stack:

Config keyDefault
server.port3000
database.urlpostgres://lumio:lumio@localhost:5432/lumio
timescale.urlpostgres://lumio:lumio@localhost:5433/lumio_tsdb
extensions.database.urlpostgres://lumio_ext:lumio_ext@localhost:5434/lumio_extensions
redis.urlredis://:lumio@127.0.0.1:6379
web.public_urlhttp://localhost:4000
admin.public_urlhttp://localhost:4001
overlay.public_urlhttp://localhost:4000/overlay
widget.public_urlhttp://localhost:4000/widget

Required public URLs for hosted API deployments

The API uses [web] public_url and [admin] public_url to mint OAuth redirect URIs and to validate the admin return target for global bot OAuth. apps/api/config/staging.toml and apps/api/config/production.toml do not pin those sections, so hosted deployments must supply them through environment variables:

EnvironmentLUMIO__WEB__PUBLIC_URLLUMIO__ADMIN__PUBLIC_URL
Productionhttps://lumio.visionhttps://admin.lumio.vision
Production previewhttps://lumio.web.prod.zaflun.devhttps://lumio.admin.prod.zaflun.dev
Staginghttps://lumio.web.staging.zaflun.devhttps://lumio.admin.staging.zaflun.dev

If LUMIO__WEB__PUBLIC_URL is missing outside local development, system-sourced channel and bot OAuth flows build provider redirect URIs with the default http://localhost:4000 host. Twitch, Kick, Trovo and YouTube reject that redirect URI in hosted environments. If LUMIO__ADMIN__PUBLIC_URL is missing, the admin global-bot flow stores http://localhost:4001/providers as its return target; the unified callback allowlist then cannot return the browser to the hosted admin app.

DATABASE_URL is not an API setting

The API reads its connection strings from the TOML layers (or LUMIO__DATABASE__URL). A bare DATABASE_URL is only used by the apps/api integration-test harness, which needs a PostgreSQL server it can CREATE DATABASE on: DATABASE_URL=postgres://lumio:lumio@127.0.0.1:5432/lumio cargo test -p lumio-api.

Next.js apps — .env.local

apps/web, apps/admin and apps/id each ship a .env.example. Copy it next to the app and fill it in:

cp apps/web/.env.example apps/web/.env.local
cp apps/admin/.env.example apps/admin/.env.local
cp apps/id/.env.example apps/id/.env.local

The variables you will actually touch first:

VariableAppDescription
LUMIO_API_URLweb, admin, idServer-side API base — http://localhost:3000/v1
LUMIO_INTERNAL_TOKENweb, admin, idShared secret for SSR → API calls; must match rate_limiting.internal_token in the API config (dev-internal-token locally)
AUTH_SECRETidNextAuth session secret
AUTH_URLidID app base URL — http://localhost:4002
AUTH_{PROVIDER}_ID / AUTH_{PROVIDER}_SECRETidOAuth login credentials per provider (see OAuth Provider Setup)
NEXT_PUBLIC_APP_URLweb, adminPublic URL of the app itself
NEXT_PUBLIC_ID_URLwebPublic URL of the ID app
NEXT_PUBLIC_OVERLAY_URLwebPublic overlay host — http://localhost:4000/overlay locally
NEXT_PUBLIC_SUPERVISOR_URL / NEXT_PUBLIC_EXTENSION_RUNTIME_URLwebExtension sandbox hosts — http://localhost:3200 / http://localhost:3201 locally
COOKIE_DOMAINweb, admin, idOnly needed for cross-subdomain deployments; leave unset locally. Must be identical across the three apps

LUMIO_WEB_URL / NEXT_PUBLIC_WEB_URL

Absolute URL of the apps/web app, read by apps/id. It is what the ID app redirects to after a successful login (/dashboard, /dashboard/connections, /account/subscription). LUMIO_WEB_URL takes precedence; NEXT_PUBLIC_WEB_URL is the client-side mirror.

  • Default in code: http://localhost:4000
  • Staging: https://lumio.web.staging.zaflun.dev
  • Production-preview: https://lumio.web.prod.zaflun.dev
  • Production: https://lumio.vision

apps/web itself does NOT need this variable — it uses relative paths because it hosts those pages.

Environment Variable Naming

Every TOML key in apps/api/config/*.toml is overridable via an environment variable using the format LUMIO__<SECTION>__<KEY>double underscore between the prefix and the first segment, and double underscore between every nested segment. Examples:

TOML pathENV var
database.urlLUMIO__DATABASE__URL
auth.token_encryption_keyLUMIO__AUTH__TOKEN_ENCRYPTION_KEY
webhooks.youtube_secretLUMIO__WEBHOOKS__YOUTUBE_SECRET
youtube.innertube_observer.api_key_overrideLUMIO__YOUTUBE__INNERTUBE_OBSERVER__API_KEY_OVERRIDE

The override priority is default.toml < {env}.toml < local.toml < ENV vars, so an env-var always wins.

Platform Credential Sources

Channel and bot OAuth flows choose their OAuth app source from the API config. The flat [platform_credentials] table applies to channel connections. The nested [platform_credentials.bot] table applies to bot connections.

# Channel-credential source. The flat form is channel.
[platform_credentials]
twitch = "system"
kick = "system"
trovo = "system"
youtube = "account"
spotify = "account"

# Bot-credential source. OAuth bots are system-first; Discord uses a static token.
[platform_credentials.bot]
twitch = "system"
kick = "system"
trovo = "system"
youtube = "system"

Allowed values are "system" and "account". system means the OAuth app pair comes from the global config keys in [auth]; account means the pair comes from the encrypted per-account app_credentials row. If a system platform has no matching global key pair configured, new connects fail safe to the account path and keep local/operator-managed installs usable.

ENV overrides use the same nested naming convention:

LUMIO__PLATFORM_CREDENTIALS__TWITCH=system
LUMIO__PLATFORM_CREDENTIALS__YOUTUBE=account
LUMIO__PLATFORM_CREDENTIALS__BOT__TWITCH=system
LUMIO__PLATFORM_CREDENTIALS__BOT__YOUTUBE=system

The global system OAuth app pairs live in [auth]:

TOML pathENV varNotes
auth.twitch_channel_client_idLUMIO__AUTH__TWITCH_CHANNEL_CLIENT_IDTwitch channel/bot system app; falls back to auth.twitch_client_id when empty
auth.twitch_channel_client_secretLUMIO__AUTH__TWITCH_CHANNEL_CLIENT_SECRETTwitch channel/bot system app secret
auth.kick_channel_client_idLUMIO__AUTH__KICK_CHANNEL_CLIENT_IDKick channel/bot system app; no login fallback
auth.kick_channel_client_secretLUMIO__AUTH__KICK_CHANNEL_CLIENT_SECRETKick channel/bot system app secret
auth.trovo_channel_client_idLUMIO__AUTH__TROVO_CHANNEL_CLIENT_IDTrovo channel/bot system app; no login fallback
auth.trovo_channel_client_secretLUMIO__AUTH__TROVO_CHANNEL_CLIENT_SECRETTrovo channel/bot system app secret

YouTube Chat Transport

YouTube chat uses InnerTube as the primary transport (zero quota cost). gRPC and REST fallbacks are available but disabled by default:

ENV varPurposeDefault
LUMIO__YOUTUBE__GRPC_FALLBACK_ENABLEDEnable gRPC streamList fallback when InnerTube failsfalse
LUMIO__YOUTUBE__REST_FALLBACK_ENABLEDEnable REST polling fallback when InnerTube and gRPC both failfalse

YouTube InnerTube Settings

The InnerTube chat poller ships with sensible defaults — no configuration is required. For operational overrides, these ENV vars map onto the [youtube.innertube_observer] block in apps/api/config/default.toml:

The InnerTube API key and client version are resolved at runtime in this order: override (if set) → Redis cache → fresh youtube.com scrape → cold-boot constant. The scraped values are cached in Redis (keys lumio:yt:innertube_key / lumio:yt:innertube_version, 24 h TTL) and a shared leader-elected refresher re-scrapes every 6 h by default (REFRESH_INTERVAL_SECS), so YouTube's periodic rotations are picked up without a restart. The two *_OVERRIDE vars below are an emergency pin only — leave them empty so auto-rotation stays in charge; a non-empty value wins over every other source, so it must never carry the ordinary current value. If an override is set, the API logs a single WARN the first time the credential is resolved — innertube: rotation disabled by pin (client_version_override=…) - unset to resume auto-rotation — so a forgotten pin never silently disables rotation (the API key value is redacted in that log line).

The *_COLD_BOOT vars are different: they configure the last-resort value used only when override, cache, and scrape have all come up empty (Stage 4). Because they are consulted last they can never pin the credential or disable auto-rotation — they are a fallback seed, not an override. default.toml ships them set to the current cold-boot values so a fresh deployment starts from a known-good credential; the daily drift-check keeps them in lockstep with the compiled constants (DEFAULT_INNERTUBE_API_KEY / DEFAULT_CLIENT_VERSION), which remain the last-ditch fallback if a field is cleared. Clear one (or set an empty ENV var) to fall back to the compiled constant, or set one to a newer value to give a fresh deployment a better cold-start credential without recompiling.

Reaching Stage 4 (cold boot) means Redis was empty and the youtube.com scrape failed — a fault, not a normal state. Point COLD_BOOT_ALERT_WEBHOOK_URL at the same Discord channel the InnerTube health CI (.github/workflows/innertube-health.yml) already posts to (do not create a second channel) and the refresher fires a single alert per incident. COLD_BOOT_ALERT_AFTER_FAILURES (default 1 = immediate) debounces it: raise it to require that many consecutive 6 h cycles at cold boot before alerting. Alerting is fail-open — an unset or unreachable webhook only produces a WARN log and never touches the chat path.

ENV varPurposeDefault
LUMIO__YOUTUBE__INNERTUBE_OBSERVER__API_KEY_OVERRIDEEmergency pin for the InnerTube API key. Empty by default; set only if auto-detection fails after a Google rotation(empty)
LUMIO__YOUTUBE__INNERTUBE_OBSERVER__CLIENT_VERSION_OVERRIDEEmergency pin for the InnerTube client version. Empty by default; set only if auto-detection fails(empty)
LUMIO__YOUTUBE__INNERTUBE_OBSERVER__API_KEY_COLD_BOOTStage-4 cold-boot API key; used only after override/cache/scrape all miss. Absent/empty → compiled DEFAULT_INNERTUBE_API_KEY. Never a pin(current key, shipped in default.toml)
LUMIO__YOUTUBE__INNERTUBE_OBSERVER__CLIENT_VERSION_COLD_BOOTStage-4 cold-boot client version; used only after override/cache/scrape all miss. Absent/empty → compiled DEFAULT_CLIENT_VERSION. Never a pin2.20260731.00.00
LUMIO__YOUTUBE__INNERTUBE_OBSERVER__CACHE_TTL_SECONDSMember + tier-badge entry TTL in Redis1209600 (14 d)
LUMIO__YOUTUBE__INNERTUBE_OBSERVER__REFRESH_INTERVAL_SECSHow often the shared refresher re-scrapes youtube.com and warms Redis21600 (6 h)
LUMIO__YOUTUBE__INNERTUBE_OBSERVER__COLD_BOOT_ALERT_WEBHOOK_URLDiscord webhook for the InnerTube health channel; alert on a Stage-4 cold-boot fall-through. Empty → alerting disabled (still WARN-logged). Reuse the CI channel, not a new one(empty)
LUMIO__YOUTUBE__INNERTUBE_OBSERVER__COLD_BOOT_ALERT_AFTER_FAILURESConsecutive cold-boot refresh cycles before alerting; 1 = immediate. 0 treated as 11

api_key_override, api_key_cold_boot, and cold_boot_alert_webhook_url are all wrapped in a SensitiveString and never appear in logs or tracing output.

Chat Retention

plans.chat_retention_days is enforced by a background sweep that hard-deletes platform_chat_messages older than each account's plan window (see Chat → Retention). It is gated so dev/test/staging environments keep their history and are not purged too aggressively.

VariablePurposeDefault
LUMIO__CHAT__RETENTION_ENFORCEMENT_ENABLEDMaster on/off switch for the retention sweep. Off in default.toml/staging; on in production.tomlfalse (dev) · true (production)
LUMIO__CHAT__RETENTION_SWEEP_INTERVAL_SECSHow often the sweep runs. Ignored when enforcement is off; a sweep also runs once on startup86400 (24 h)

Automation Worker

The API dispatches extension automation-node handlers to the Automation Worker over HTTP. Configured under [automation_worker] in apps/api/config/default.toml.

VariablePurposeDefault
LUMIO__AUTOMATION_WORKER__URLBase URL of the Automation Worker (apps/automation-worker, no trailing slash)http://automation-worker:8091
LUMIO__AUTOMATION_WORKER__SYSTEM_KEYSystem key presented as Authorization: SystemKey <key>; must match the worker's worker.system_key. Empty (the default) disables extension-node dispatch and fails closed(empty)

Handler-execution telemetry (workers)

Both handler-execution workers (apps/bot-module-worker, apps/automation-worker) optionally record per-execution telemetry into TimescaleDB — one extension_errors row on failure and one extension_install_logs row on every run (see Developer Extension Endpoints). The writer is fail-open: it never fails or delays a handler request, and is simply disabled when no TimescaleDB pool is configured. Env vars use each worker's own prefix — BOT_MODULE_WORKER__… and AUTOMATION_WORKER__….

VariablePurposeDefault
…__TIMESCALE__URLTimescaleDB connection URL. Omit the whole [timescale] section to disable the telemetry writer(unset → writer off)
…__WORKER__TELEMETRY_SALTPer-deployment secret salt used to HMAC-anonymize account/install IDs in extension_errors. Set a stable value in production; an empty salt still hashes but is weaker(empty)

Worker registry sync

Each worker (apps/bot-module-worker, apps/automation-worker) populates its in-memory InstallRegistry from the API's internal handler-bundle feed on startup and re-syncs on a poll — see Bot Modules → Worker sync-loop. Configured under [sync], with the env prefix BOT_MODULE_WORKER__SYNC__* and AUTOMATION_WORKER__SYNC__* respectively. When the section is not fully configured the worker starts with an empty registry (every request short-circuits) rather than failing.

Variable (per worker prefix)PurposeDefault
…__SYNC__ENABLEDRun the registry sync looptrue
…__SYNC__API_BASE_URLAPI origin the feed is served from (no trailing slash), e.g. http://api:8080. Empty disables the loop(empty)
…__SYNC__SYSTEM_KEYSystem key presented to the feed as Authorization: Bearer <key>. Empty disables the loop(empty)
…__SYNC__POLL_INTERVAL_SECSFull-snapshot re-sync interval30

GeoIP

The optional GeoIP service resolves client IP addresses to a country/city using the free MaxMind GeoLite2 City database. It powers the Location column of the Audit Log. It is disabled by default — no MaxMind account is needed to run Lumio; leave enabled = false and the location fields simply stay empty.

When enabled with a license key, the API downloads the .mmdb database on startup if it is missing and, when auto_update is on, refreshes it every update_interval_days. All GeoIP state lives under [geoip] in apps/api/config/default.toml; every key is overridable via the LUMIO__GEOIP__* env vars below.

VariablePurposeDefault
LUMIO__GEOIP__ENABLEDMaster on/off switch. When false, no database is loaded and location fields stay emptyfalse
LUMIO__GEOIP__DATABASE_PATHPath to the GeoLite2 City .mmdb file. The default lives under the container WORKDIR (/app); the runtime-base image pre-creates /app/data/geoip owned by uid 1001, so the file is downloadable and refreshable by the non-root user. Mount a persistent volume there to survive restarts/app/data/geoip/GeoLite2-City.mmdb
LUMIO__GEOIP__LICENSE_KEYMaxMind license key used to download/update the database. Get a free key. Without it, an existing database is still read but cannot be auto-downloaded(empty)
LUMIO__GEOIP__ACCOUNT_IDMaxMind numeric account ID. Required (with the license key) for the Privacy Exclusions API(empty)
LUMIO__GEOIP__AUTO_UPDATERe-download the database on a schedule when it is older than update_interval_daysfalse
LUMIO__GEOIP__UPDATE_INTERVAL_DAYSMinimum database age before a re-download. MaxMind publishes GeoLite2 weekly7
LUMIO__GEOIP__PRIVACY_EXCLUSIONS_ENABLEDHonour MaxMind Privacy Exclusions (user geolocation opt-outs). Requires license_key + account_idfalse
LUMIO__GEOIP__PRIVACY_EXCLUSIONS_REFRESH_HOURSHow often the cached exclusions list is refreshed24
LUMIO__GEOIP__PRIVACY_EXCLUSIONS_CACHE_PATHWhere the exclusions list is cached for persistence across restarts. Lives on the same writable /app/data/geoip directory as the database/app/data/geoip/geoip-privacy-exclusions.json

For a one-off database refresh outside the server, run the geoip-update maintenance binary: cargo run -p lumio-api --bin geoip-update -- --help.

Privacy. GeoIP derives only a coarse country/city from the IP — no precise coordinates are stored. Enabling privacy_exclusions_enabled makes Lumio respect MaxMind's Privacy Exclusions opt-out list so IPs of users who opted out are not resolved.

Frontend Environment & Logging

apps/web, apps/admin, and apps/id share two env-vars that drive the logger and the Sentry integration. Set both the server-side and the NEXT_PUBLIC_* mirror so the browser bundle picks them up too.

VariableAllowed valuesDefaultPurpose
LUMIO_ENV / NEXT_PUBLIC_LUMIO_ENVdevelopment | staging | productiondevelopmentLogger threshold default + Sentry environment tag
LUMIO_LOG_LEVEL / NEXT_PUBLIC_LUMIO_LOG_LEVELdebug | info | warn | errorper env (debug/info/warn)Override the auto-threshold

Sentry env-vars per app — leave blank locally to disable, fill them in the CI build / deployment environment for staging + production:

VariableWherePurpose
NEXT_PUBLIC_SENTRY_DSNBrowser + serverPublic DSN
SENTRY_DSNServer onlyOptional override; falls back to NEXT_PUBLIC_SENTRY_DSN
SENTRY_ORGBuild (CI)Sentry org slug for source-map upload
SENTRY_PROJECTBuild (CI)Sentry project slug
SENTRY_AUTH_TOKENBuild (CI)Auth token for source-map upload

Each app also has a proxy.ts (Next.js 16's renamed middleware.ts) that prints one structured line per HTTP request to the SSR terminal — including the real client IP (x-real-ip / x-forwarded-for / cf-connecting-ip) with IPv4/IPv6 family classification. See Logging → Per-request access logging for details.

Verify Installation

  1. just stack-up — all six containers report healthy.
  2. just migrate-status — every migration shows as applied.
  3. just run-api — the API answers on http://localhost:3000/v1. GET /v1/graphiql serves the GraphiQL IDE and GET /v1/schema the GraphQL SDL (both are on by default in development and gated by [graphql].playground / [graphql].schema_sdl). The REST surface is documented at /v1/swagger-ui/ with the raw spec at /v1/openapi.json.
  4. just dev-web — visit http://localhost:4000 and log in through the ID app on http://localhost:4002.

Before you push

RecipeWhat it runs
just verify-allcargo fmt --check + clippy (-D warnings) + cargo check + ESLint + tsc — the fast inner loop
just lint-allclippy + ESLint only
just test-allcargo test (workspace) + the V8 extension tests + Vitest
just verify-releaseverify-all plus regenerating the OpenAPI/GraphQL schemas and the Bazel vendor tree, failing on drift. Run this after a version bump or a Cargo.lock change — verify-all does not detect that drift