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
| Variable | Required | Description |
|---|---|---|
HUB_ADMIN_SECRET | Required | Hub admin login password. Use 32+ random characters. |
HUB_SCHOOL_SECRET | Required | HMAC secret for school connection tokens. |
DEV_PUBLIC_KEY | Required | Ed25519 public key (PEM or base64url SPKI) for verifying signed game ZIPs and license keys. |
BETTER_AUTH_SECRET | Required | Session secret for Better Auth (developer/parent/org email accounts). |
BETTER_AUTH_URL | Recommended | Public URL of the Hub, used by Better Auth for cookies and callbacks. |
HUB_ORG_JWT_SECRET | Recommended | Signing secret for Organisation Portal JWTs. Org login fails in production without it. |
DEV_PRIVATE_KEY | Recommended | Ed25519 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)
| Variable | Required | Description |
|---|---|---|
DEV_PUBLIC_KEYS | Optional | Additional 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
| Variable | Default | Description |
|---|---|---|
AUTH_SESSION_TTL_DAYS | 30 | Better 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
| Variable | Default | Description |
|---|---|---|
HUB_AI_DATA_DIR | data/ai | Directory where provisioned AI assets are stored (on Fly.io this lives on the persistent volume). |
HUB_AI_AUTO_PROVISION | true | Set 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)
| Variable | Default | Description |
|---|---|---|
HUB_PORT | 3000 | Port the hub listens on (also reads PORT). The production fly.toml sets it to 4000 to match internal_port. |
HUB_PUBLIC_URL | https://ubuntuplay.fly.dev | Full public URL used for credentials, email links, and Flutterwave callbacks. |
HUB_ORIGIN | https://ubuntuplay.fly.dev | CORS 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). |
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
| Variable | Required | Description |
|---|---|---|
FLUTTERWAVE_SECRET_KEY | For billing | Flutterwave secret key from dashboard.flutterwave.com → Settings → API Keys. Billing routes degrade gracefully without it. |
FLUTTERWAVE_PUBLIC_KEY | For billing | Flutterwave public key. |
FLUTTERWAVE_WEBHOOK_SECRET | Recommended | Webhook verification hash. Set in Flutterwave dashboard under Webhooks. Prevents spoofed payment confirmations. |
BILLING_CURRENCY | Optional | Default billing currency. ZMW or USD. Default: ZMW. |
Email — Resend
| Variable | Required | Description |
|---|---|---|
RESEND_API_KEY | Optional | Enables 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=...
| Variable | Required | Description |
|---|---|---|
BACKUP_SCHEDULE_ENABLED | Optional | Starts 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_KEY | Optional | Off-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_PREFIX | Optional | Region (default auto) and object key prefix for replicated backups. |
Classroom Server
The classroom server (classroom/server.js) reads its own, smaller set:
| Variable | Default | Description |
|---|---|---|
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). |
PORT | 3000 | HTTP listener port. |
HTTPS_PORT | 3443 | HTTPS 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_PORT | 3001 | Gogo 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_MS | 300000 | Heartbeat sync interval in milliseconds when online (default 5 minutes, floor 60 seconds). |
Auto-generated secrets
JWT_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
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.