Skip to content

Latest commit

 

History

History
222 lines (166 loc) · 11.6 KB

File metadata and controls

222 lines (166 loc) · 11.6 KB

Deployment


Standalone Server

The standalone app (apps/standalone/src/index.ts) reads all configuration from environment variables and starts a Bun.serve listener. This is what the Docker image runs.

Without Docker — npx/bunx serve

The same server ships in the published package as the CLI serve command, so you can run it without building or containerizing anything:

PORT=4000 \
  SOURCE_STORAGE_DRIVER=redis RUNTIME_STORAGE_DRIVER=redis \
  REDIS_URL=redis://localhost:6379 \
  AUTH_MODE=basic AUTH_USERNAME=admin AUTH_PASSWORD=secret \
  npx @xtandard/flags serve          # or: bunx @xtandard/flags serve

serve honors the same environment variables documented below and works under both Node (npx, via a node:http bridge) and Bun (bunx, via Bun.serve). It serves the bundled SPA from the package's dist/ui, the JSON API, OFREP, the OpenAPI document, and GET /healthcheck. Use it for local development, a single small node, or anywhere a container is overkill; reach for the Docker image when you want a pinned, reproducible runtime.

Where does it store data? Unlike the Docker image (which defaults to memory), the CLI defaults to file storage under ./.flags/ in the current working directory — ./.flags/source (drafts/history/audit) and ./.flags/runtime (published snapshots), created lazily on first write. serve prints the resolved paths on startup:

[xtandard/flags] storage source:  file → /abs/path/.flags/source
[xtandard/flags] storage runtime: file → /abs/path/.flags/runtime

Because the default is relative to the cwd, set an absolute SOURCE_FILE_DIR / RUNTIME_FILE_DIR (or a non-file driver) for a stable location.

Environment Variables

Variable Default Description
PORT 3000 TCP port to listen on.
BASE_PATH "" URL prefix, e.g. "/flags". Must match the path you expose.
TITLE "@xtandard/flags" Navbar wordmark, shown in the admin UI and /healthcheck.
LOGO_URL "" Logo image URL shown in place of the title wordmark.
SOURCE_STORAGE_DRIVER "memory" Source driver: memory, file, redis, postgres, mongodb, sqlite, unstorage.
RUNTIME_STORAGE_DRIVER "memory" Runtime driver (same set as above).
REDIS_URL "redis://localhost:6379" Redis connection URL (used when driver is redis).
SOURCE_PREFIX "xtandard:flags:source" Redis key prefix for source storage.
RUNTIME_PREFIX "xtandard:flags:runtime" Redis key prefix for runtime storage.
SOURCE_FILE_DIR "./data/source" Directory for file-backed source storage.
RUNTIME_FILE_DIR "./data/runtime" Directory for file-backed runtime storage.
AUTH_MODE "none" Authentication mode: none or basic.
AUTH_USERNAME "admin" Username for basic auth mode.
AUTH_PASSWORD_HASH "" scrypt hash of the admin password (preferred). See Auth.
AUTH_PASSWORD "" Plaintext password for basic auth (dev only).
READONLY false Set to 1 or true to block all mutating operations.
STREAMING false Set to 1 or true to enable the opt-in OFREP SSE stream (/ofrep/v1/stream).
SNAPSHOT_KEEP_LAST unset (keep all) Retention: keep at most the N most recent snapshots (pruned after each publish).
SNAPSHOT_MAX_AGE unset (keep all) Retention: keep snapshots newer than this (<n><s|m|h|d>, e.g. 34h, 30d).
AUDIT_KEEP_LAST unset (keep all) Retention: keep at most the N most recent audit entries.
AUDIT_MAX_AGE unset (keep all) Retention: keep audit entries newer than this (<n><s|m|h|d>).
UI_DIR resolved from module path Override the bundled UI directory (e.g. in custom Docker images).

When both a *_KEEP_LAST and a *_MAX_AGE are set, an item is kept if either rule keeps it (union). The active and latest snapshots are always kept. Removed payloads are surfaced to after hooks (snapshot.deleted / audit.pruned) for offloading — see Hooks.

Healthcheck

The standalone server responds to GET /healthcheck (and GET /{basePath}/healthcheck) with:

{ "status": "ok", "title": "@xtandard/flags" }

This is independent of auth — no credentials are required for the healthcheck endpoint.


Docker

Build

Build from the repository root (the Dockerfile context must be the root):

docker build -f apps/standalone/Dockerfile -t xtandard-flags .

The build is multi-stage:

  1. build stage: installs dependencies and runs bun run build:ui.
  2. runtime stage: copies src/, dist/ui/, node_modules/, and apps/standalone/ — no compiled lib needed because Bun runs TypeScript directly.

Run

Minimal (in-memory storage, no auth — for local testing only):

docker run --rm -p 3000:3000 xtandard-flags

With Redis and basic auth:

docker run --rm -p 3000:3000 \
  -e SOURCE_STORAGE_DRIVER=redis \
  -e RUNTIME_STORAGE_DRIVER=redis \
  -e REDIS_URL=redis://host.docker.internal:6379 \
  -e AUTH_MODE=basic \
  -e AUTH_USERNAME=admin \
  -e AUTH_PASSWORD_HASH='scrypt$abc123...$def456...' \
  xtandard-flags

Docker Compose with Redis

# docker-compose.yml
version: "3.9"
services:
  redis:
    image: redis:7-alpine
    command: redis-server --notify-keyspace-events KEA
    ports:
      - "6379:6379"

  flags:
    build:
      context: .
      dockerfile: apps/standalone/Dockerfile
    ports:
      - "3000:3000"
    environment:
      SOURCE_STORAGE_DRIVER: redis
      RUNTIME_STORAGE_DRIVER: redis
      REDIS_URL: redis://redis:6379
      SOURCE_PREFIX: xtandard:flags:source
      RUNTIME_PREFIX: xtandard:flags:runtime
      AUTH_MODE: basic
      AUTH_USERNAME: admin
      AUTH_PASSWORD_HASH: "${FLAGS_ADMIN_PASSWORD_HASH}"
    depends_on:
      - redis

The notify-keyspace-events KEA flag on Redis enables keyspace notifications, which allows the OpenFeature provider to pick up new snapshots via watch instead of waiting for the next poll.


Security Warnings

Authentication

Always set AUTH_MODE=basic (or provide a custom AuthProvider) before exposing the admin panel to any network.

When AUTH_MODE=none the standalone server logs:

[xtandard/flags] Running with AUTH_MODE=none. Do NOT expose this publicly without authentication.

A deployment with no auth and a publicly reachable port allows anyone to read, modify, and publish flags.

Password Storage

  • Use AUTH_PASSWORD_HASH (scrypt) rather than AUTH_PASSWORD (plaintext) in production.
  • Generate a hash with:
bun -e "const {hashPassword} = await import('./src/auth/basic.ts'); console.log(await hashPassword('your-password'))"
  • Store the hash in an environment secret (CI secret, Docker secret, Vault), not in source control.

Readonly Mode

When the admin is embedded in an application that untrusted code can reach, set readonly: true (or READONLY=1). This blocks all state-changing API calls at the core level, returning 403 for any mutating action regardless of auth.

Cookie/Session Auth and CSRF

If you implement a delegated AuthProvider that uses cookies or sessions, add CSRF protection to your outer request handler. The panel handler does not include CSRF middleware — it is the consumer's responsibility when session-based auth is used.

Network Exposure

Consider mounting the admin panel on an internal-only port or path:

  • Use BASE_PATH to namespace the panel under a non-obvious route.
  • Place a reverse proxy in front and restrict access by IP or mTLS.
  • Run the standalone container on a non-public network and only expose the OpenFeature provider's runtime storage (Redis) to applications.

Storage driver env vars (per driver)

The standalone app and CLI accept these SOURCE_STORAGE_DRIVER / RUNTIME_STORAGE_DRIVER values. Source and runtime share one connection but are isolated by a distinct table/collection/prefix/dir.

Driver Env vars
memory (none) — ephemeral, lost on restart. Docker default.
file SOURCE_FILE_DIR / RUNTIME_FILE_DIR (standalone default ./data/{source,runtime}; CLI serve default ./.flags/{source,runtime})
redis REDIS_URL (default redis://localhost:6379); optional SOURCE_PREFIX / RUNTIME_PREFIX
postgres DATABASE_URL (or POSTGRES_URL); optional SOURCE_PG_TABLE / RUNTIME_PG_TABLE (default xtandard_flags_{source,runtime})
mongodb MONGO_URL; optional MONGO_DB (default xtandard_flags), SOURCE_MONGO_COLLECTION / RUNTIME_MONGO_COLLECTION (default flags_{source,runtime})
sqlite SOURCE_SQLITE_PATH / RUNTIME_SQLITE_PATH (default ./data/{source,runtime}.sqlite). Bun-only — works in the Docker image (Bun) and under bunx.
unstorage in-memory by default; configure other unstorage drivers in code (see Storage)

Note: libsql/Turso and cloudflare-kv adapters exist but are wired in code (createLibsqlStorage / createCloudflareKvStorage), not via these env drivers.

# Postgres source + runtime
docker run -p 3000:3000 \
  -e SOURCE_STORAGE_DRIVER=postgres -e RUNTIME_STORAGE_DRIVER=postgres \
  -e DATABASE_URL=postgres://user:pass@db:5432/flags \
  ghcr.io/xantiagoma/xtandard-flags:latest

# MongoDB source + runtime
docker run -p 3000:3000 \
  -e SOURCE_STORAGE_DRIVER=mongodb -e RUNTIME_STORAGE_DRIVER=mongodb \
  -e MONGO_URL=mongodb://db:27017 \
  ghcr.io/xantiagoma/xtandard-flags:latest

A common production split: Postgres as source (durable, transactional history) and Redis/Upstash as runtime (fast reads).