Docs
search Esc

Environment Variables

Configure the Hub and Classroom servers using environment variables. Set them in a .env file for local development, or via fly secrets set for Fly.io deployments.

Required (Hub, production)

In production (NODE_ENV other than development) the hub refuses to start without these four variables. In development, stable local fallbacks are generated automatically:

BASH
HUB_ADMIN_SECRET=your-strong-admin-password
HUB_SCHOOL_SECRET=another-strong-secret-for-schools
DEV_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----
..."
BETTER_AUTH_SECRET=long-random-session-secret
VariableRequiredDescription
HUB_ADMIN_SECRETRequiredHub admin login password. Use 32+ random characters.
HUB_SCHOOL_SECRETRequiredHMAC secret for school connection tokens.
DEV_PUBLIC_KEYRequiredEd25519 public key (PEM or base64url SPKI) for verifying signed game ZIPs and license keys.
BETTER_AUTH_SECRETRequiredSession secret for Better Auth (developer/parent/org email accounts).
BETTER_AUTH_URLRecommendedPublic URL of the Hub, used by Better Auth for cookies and callbacks.
HUB_ORG_JWT_SECRETRecommendedSigning secret for Organisation Portal JWTs. Org login fails in production without it.
DEV_PRIVATE_KEYRecommendedEd25519 private key for signing licenses and games. Without it, license generation is disabled. Keep this secret.

Licensing & Key Rotation

BASH
DEV_PUBLIC_KEYS="key-one key-two"   # extra trusted keys (space/comma/semicolon separated)
VariableRequiredDescription
DEV_PUBLIC_KEYSOptionalAdditional trusted Ed25519 public keys (base64url SPKI list). Licenses validate against ANY trusted key, so a new signing key can be trusted before the old one is retired. Supported by both Hub and Classroom servers.

Sessions

VariableDefaultDescription
AUTH_SESSION_TTL_DAYS30Better Auth session lifetime in days (rolling — refreshed daily while in use). The native Android apps rely on long-lived sessions; lowering this logs families and org admins out sooner.

Hub AI — Browser-Local Engine

When the Hub Admin enables AI, the hub downloads the LiteRT-LM runtime, the Gemma model, and vision assets (~2 GB total) to its data volume and serves them to visitors' browsers, which run the engine via WebGPU. See Offline AI →.

BASH
HUB_AI_DATA_DIR=            # Where AI assets are stored (default: data/ai)
HUB_AI_AUTO_PROVISION=      # "false" disables auto-download on toggle/boot
HUB_AI_CLIENT_MODEL_URL=    # Hand browsers an external model URL (egress relief)
HUB_AI_ASSET_MANIFEST=      # Path to a JSON manifest overriding the asset registry
VariableDefaultDescription
HUB_AI_DATA_DIRdata/aiDirectory where provisioned AI assets are stored (on Fly.io this lives on the persistent volume).
HUB_AI_AUTO_PROVISIONtrueSet to false to stop the hub auto-downloading assets when AI is toggled on or when an interrupted download is found at boot.
HUB_AI_CLIENT_MODEL_URL—Optional external URL browsers should download the model from instead of the hub-hosted copy (saves hub egress; e.g. a direct Hugging Face link).
HUB_AI_ASSET_MANIFEST—Path to a JSON file ({"assets": [...]}) replacing the built-in asset registry. Operator-controlled only; used by tests and mirrored deployments.

Network & Access

BASH
HUB_PORT=3000               # Port to listen on (code default: 3000; production fly.toml sets 4000)
HUB_PUBLIC_URL=https://ubuntuplay.fly.dev  # Official public Hub URL
HUB_ORIGIN=https://...      # Allowed CORS origin (set to your public URL)
HUB_TRUSTED_ORIGINS=...     # Extra comma-separated trusted origins (LAN/native apps)
VariableDefaultDescription
HUB_PORT3000Port the hub listens on (also reads PORT). The production fly.toml sets it to 4000 to match internal_port.
HUB_PUBLIC_URLhttps://ubuntuplay.fly.devFull public URL used for credentials, email links, and Flutterwave callbacks.
HUB_ORIGINhttps://ubuntuplay.fly.devCORS allowed origin. Set it to your Hub URL for private deployments.
HUB_TRUSTED_ORIGINS—Comma-separated list of extra origins trusted by Better Auth (for LAN or native-app deployments).
warning
HUB_ORIGIN is criticalIf HUB_ORIGIN is not set, the hub allows all origins (open CORS), which is fine for development but not ideal for production. Set it to your public URL.

Billing — Flutterwave

BASH
FLUTTERWAVE_SECRET_KEY=FLWSECK_LIVE-...
FLUTTERWAVE_PUBLIC_KEY=FLWPUBK_LIVE-...
FLUTTERWAVE_WEBHOOK_SECRET=your-webhook-hash
BILLING_CURRENCY=ZMW
VariableRequiredDescription
FLUTTERWAVE_SECRET_KEYFor billingFlutterwave secret key from dashboard.flutterwave.com → Settings → API Keys. Billing routes degrade gracefully without it.
FLUTTERWAVE_PUBLIC_KEYFor billingFlutterwave public key.
FLUTTERWAVE_WEBHOOK_SECRETRecommendedWebhook verification hash. Set in Flutterwave dashboard under Webhooks. Prevents spoofed payment confirmations.
BILLING_CURRENCYOptionalDefault billing currency. ZMW or USD. Default: ZMW.

Email — Resend

VariableRequiredDescription
RESEND_API_KEYOptionalEnables transactional email (license expiry notices, welcome emails, weekly family digests). Email features are disabled without it.

Backups

BASH
BACKUP_SCHEDULE_ENABLED=true    # daily 02:00 encrypted backups
BACKUP_S3_ENDPOINT=https://...  # optional off-site replication (S3-compatible)
BACKUP_S3_BUCKET=ubuntuplay-backups
BACKUP_S3_ACCESS_KEY=...
BACKUP_S3_SECRET_KEY=...
VariableRequiredDescription
BACKUP_SCHEDULE_ENABLEDOptionalStarts the scheduled encrypted backup system. Admin choices persisted in data/backup_config.json take precedence over this variable.
BACKUP_S3_ENDPOINT / BACKUP_S3_BUCKET / BACKUP_S3_ACCESS_KEY / BACKUP_S3_SECRET_KEYOptionalOff-site replication to any S3-compatible store. All four must be set to activate; replication failures never fail the local backup.
BACKUP_S3_REGION / BACKUP_S3_PREFIXOptionalRegion (default auto) and object key prefix for replicated backups.

Classroom Server

The classroom server (classroom/server.js) reads its own, smaller set:

VariableDefaultDescription
DEV_PUBLIC_KEY—Ed25519 public key for license validation and signed ZIP verification. The server starts without it, but license activation and game uploads are disabled. On Android builds, data/dev_public_key.txt (one key per line) replaces the env trust set when present.
DEV_PUBLIC_KEYS—Additional trusted public keys for rotation (same format as the Hub).
PORT3000HTTP listener port.
HTTPS_PORT3443HTTPS listener with an auto-generated self-signed certificate.
DISABLE_HTTPS—Set to 1 to disable the HTTPS listener.
DISABLE_HTTPS_REDIRECT—Set to 1 to stop HTTP browser navigations redirecting to HTTPS.
WS_BRIDGE_PORT3001Gogo AI Bridge WebSocket port (also served as wss://host:HTTPS_PORT/gogo-bridge). The bridge page discovers the live port via GET /api/ai/bridge-info.
HUB_HEARTBEAT_MS300000Heartbeat sync interval in milliseconds when online (default 5 minutes, floor 60 seconds).
info
Auto-generated secretsJWT_SECRET and ANON_SECRET are generated on first run and stored in classroom/data/. Do not set them manually.

Generating Keys

Admin and school secrets

BASH
# Run this twice — use output for HUB_ADMIN_SECRET and HUB_SCHOOL_SECRET
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

Ed25519 key pair

BASH
node << 'EOF'
const c = require('crypto');
const { privateKey, publicKey } = c.generateKeyPairSync('ed25519', {
  privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
  publicKeyEncoding:  { type: 'spki',  format: 'pem' }
});
console.log('DEV_PRIVATE_KEY:'); console.log(privateKey);
console.log('DEV_PUBLIC_KEY:');  console.log(publicKey);
EOF

Setting Secrets on Fly.io

BASH
# Set multiple secrets at once
fly secrets set \
  HUB_ADMIN_SECRET="..." \
  HUB_SCHOOL_SECRET="..." \
  HUB_ORIGIN="https://your-app.fly.dev"

# List secret keys (values are hidden)
fly secrets list

# Remove a secret
fly secrets unset HUB_ACCESS_PATH
lightbulb
Local developmentCreate a .env file in the hub directory and use node -r dotenv/config hub.js to load it. Never commit .env to Git — add it to .gitignore.