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.
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 serveserve 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.serveprints the resolved paths on startup:[xtandard/flags] storage source: file → /abs/path/.flags/source [xtandard/flags] storage runtime: file → /abs/path/.flags/runtimeBecause 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.
| 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.
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.
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:
buildstage: installs dependencies and runsbun run build:ui.runtimestage: copiessrc/,dist/ui/,node_modules/, andapps/standalone/— no compiled lib needed because Bun runs TypeScript directly.
Minimal (in-memory storage, no auth — for local testing only):
docker run --rm -p 3000:3000 xtandard-flagsWith 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.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:
- redisThe 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.
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.
- Use
AUTH_PASSWORD_HASH(scrypt) rather thanAUTH_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.
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.
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.
Consider mounting the admin panel on an internal-only port or path:
- Use
BASE_PATHto 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.
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 andcloudflare-kvadapters 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:latestA common production split: Postgres as source (durable, transactional history)
and Redis/Upstash as runtime (fast reads).