owlette docs
self-hosting

web deployment

The owlette dashboard is a Next.js application in web/. This guide covers Railway, Vercel, and generic Node.js hosting.


runtime baseline

Use Node.js 22.x and npm 10 for the web app build. The web/package.json engines pin is the binding constraint for npm run build.

sourcebaseline
web/package.json enginesnode 22.x
Repository root package.jsonnode >=20.9.0, npm >=10.0.0
Repository .nvmrc20.18.0
web/nixpacks.tomlnodejs_20, npm-10_x
Firebase FunctionsNode 20

For a fresh host, select Node 22.x. The root package.json, .nvmrc, web/nixpacks.toml, and Firebase Functions still reference Node 20, but the web build's engines pin (22.x) overrides the nixpacks nodejs_20 package, so the build runs on Node 22. Node 20 reproduces a next build failure (MODULE_NOT_FOUND from fumadocs-mdx).


step 1: create railway project

  1. Go to railway.app and sign up with GitHub.
  2. Click New Project -> Deploy from GitHub repo.
  3. Select the owlette repository.

step 2: configure service

  1. Click your service -> Settings.
  2. Set Root Directory to web.
  3. Set Branch to main for production or dev for development.
  4. Enable Auto-deploy on push.

The repository includes the deployment configuration Railway should use:

# web/railway.toml
[build]
builder = "NIXPACKS"
buildCommand = "npm run build"

[deploy]
startCommand = "npm start"
restartPolicyType = "ON_FAILURE"
restartPolicyMaxRetries = 10

[env]
NODE_ENV = "production"

Install is handled by Nixpacks, not by railway.toml:

# web/nixpacks.toml
[phases.setup]
nixPkgs = ["nodejs_20", "npm-10_x"]

[phases.install]
cmds = ["npm ci --legacy-peer-deps"]

step 3: add environment variables

In Railway -> your service -> Variables, add the production values from Environment Variables. A working web deployment needs these runtime groups:

  • Firebase client: NEXT_PUBLIC_FIREBASE_API_KEY, NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN, NEXT_PUBLIC_FIREBASE_PROJECT_ID, NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET, NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID, NEXT_PUBLIC_FIREBASE_APP_ID
  • Firebase Admin: FIREBASE_PROJECT_ID, FIREBASE_CLIENT_EMAIL, FIREBASE_PRIVATE_KEY
  • Session and encryption: SESSION_SECRET, MFA_ENCRYPTION_KEY, LLM_ENCRYPTION_KEY
  • R2 for roost uploads and version bodies: R2_S3_ENDPOINT, R2_S3_ACCESS_KEY_ID, R2_S3_SECRET_ACCESS_KEY
  • Rate limiting: UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN
  • Public URL: NEXT_PUBLIC_BASE_URL
  • Cron: CRON_SECRET
  • Email alerts: RESEND_API_KEY, RESEND_FROM_EMAIL, ADMIN_EMAIL_PROD, ADMIN_EMAIL_DEV
  • Public status page: INSTATUS_API_KEY, INSTATUS_PAGE_ID, and the INSTATUS_COMPONENT_*_ID values when /api/cron/status-ping should publish to Instatus
  • Autonomous Cortex: CORTEX_INTERNAL_SECRET when autonomous alert investigation is enabled

RAILWAY_PUBLIC_DOMAIN, RAILWAY_ENVIRONMENT, PORT, and NODE_ENV are injected by Railway or by web/railway.toml. Set ROOST_ENV=prod or ROOST_ENV=dev only when the automatic roost bucket selection does not match the service.

NEXT_PUBLIC_* variables are read during the Next.js build and by the browser. After changing them, redeploy so the built client bundle receives the new values.

step 4: deploy

Railway deploys automatically after variables are configured. Monitor the build in the Deployments tab.

Expected build time is 2-5 minutes.

step 5: configure firebase auth domain

  1. Copy your Railway deployment URL.
  2. Go to Firebase Console -> Authentication -> Settings -> Authorized Domains.
  3. Add your Railway URL, for example owlette-web.up.railway.app.

Required

Without this step, users cannot log in because Firebase rejects auth requests from unauthorized domains.

step 6: custom domain (optional)

  1. Railway -> Settings -> Networking -> Add Custom Domain.
  2. Add the CNAME or A record as instructed.
  3. Railway provisions SSL automatically.
  4. Add the custom domain to Firebase Authorized Domains.
  5. Update NEXT_PUBLIC_BASE_URL to the custom HTTPS origin and redeploy.

vercel deployment

Vercel can run the web app as a standard Next.js project.

step 1: import project

  1. In Vercel, click Add New -> Project.
  2. Import the owlette repository.
  3. Set Root Directory to web.
  4. Set the framework preset to Next.js.

step 2: configure build settings

Use the same commands as the checked-in Railway/Nixpacks deployment:

settingvalue
Install Commandnpm ci --legacy-peer-deps
Build Commandnpm run build
Output DirectoryNext.js default; leave unset unless Vercel asks for an explicit value

Set the Node.js version to 22.x in the Vercel project settings.

step 3: add environment variables

Add the same variables listed in the Railway section under Settings -> Environment Variables. Apply them to Production and Preview as needed.

NEXT_PUBLIC_* values are bundled at build time, so redeploy after changing Firebase client settings or NEXT_PUBLIC_BASE_URL.

step 4: configure firebase auth domain

Add the Vercel deployment domain and any custom domain to Firebase Authentication -> Settings -> Authorized Domains.

step 5: cron

The current cron routes require X-Cron-Secret: <CRON_SECRET>. Use a scheduler that can send that header. If the hosting plan cannot send custom headers or cannot call all required schedules, use an external scheduler.


cron health checks

Set up the machine offline detection cron:

step 1: add cron_secret

python -c "import secrets; print(secrets.token_hex(32))"

Add the generated value as CRON_SECRET in the host environment variables.

step 2: configure cron schedule

Call the health-check route every 5 minutes:

GET https://<your-app>/api/cron/health-check
X-Cron-Secret: <CRON_SECRET value>

Use cron expression */5 * * * * when the host accepts cron syntax.

Format

Use spaces between fields: */5 * * * *. No spaces (*/5****) will fail.

alert digest crons

Two routes batch-and-send the agent's queued alerts. If they are not scheduled, those emails never go out — the agent only queues alerts (into pending_process_alerts / pending_display_alerts); these crons are what actually email them. Schedule both, in every environment, every 3 minutes:

GET https://<your-app>/api/cron/process-alerts
GET https://<your-app>/api/cron/display-alerts
X-Cron-Secret: <CRON_SECRET value>

Use cron expression */3 * * * *. A missing schedule here fails silently — the queue just grows and nobody is notified — so confirm it per environment (dev and prod).

public status-page ping

External public launch also needs a 60-second HTTP cron that calls:

GET https://<your-app>/api/cron/status-ping
X-Cron-Secret: <CRON_SECRET value>

The route writes an internal status-ping record and posts component status changes to Instatus when the INSTATUS_* variables are configured. It waits for two consecutive failures before degrading a component, then marks it operational on recovery. Publish failures are logged and summarized in the cron response without returning vendor error bodies or failing the local ping write.

Use a separate cron job or service if the hosting provider only supports one schedule per service. Keep /api/cron/status-ping internal; it is not part of the public API reference.

After adding the status-page variables, run the readiness check from an operator shell with the same env values loaded:

node scripts/check-status-page-ready.mjs --env-only
node scripts/check-status-page-ready.mjs --base-url https://<your-app>

The check validates required env names, component ids, and the live status-ping response without printing secrets.


two-branch deployment

owlette uses two branches with separate deployments:

branchdeploymenturl
devDevelopmentdev.owlette.app
mainProductionowlette.app

Each branch should have its own service or project, environment variables, and optionally its own Firebase project.


general node.js hosting

For non-Railway and non-Vercel deployments:

cd web
npm ci --legacy-peer-deps
npm run build
npm start

Requirements:

  • Node.js 22.x, npm 10.x
  • All required environment variables from Environment Variables
  • A writable port supplied as PORT or the default Next.js port 3000
  • HTTPS for Firebase Auth in production

deployment checklist

pre-deployment

  • Host is configured for Node.js 22.x and npm 10.x
  • npm ci --legacy-peer-deps succeeds
  • npm run build succeeds
  • Required environment variables are set for the target environment
  • NEXT_PUBLIC_BASE_URL matches the public HTTPS origin

host setup

  • Repository linked
  • Root directory set to web
  • Branch configured
  • Install command is npm ci --legacy-peer-deps
  • Build command is npm run build
  • Start command is npm start where the host requires one
  • CRON_SECRET configured
  • Cron calls configured for all four routes, in each environment — /api/cron/health-check (every 5 min), /api/cron/process-alerts (every 3 min), /api/cron/display-alerts (every 3 min), /api/cron/status-ping (every 60 s)

post-deployment

  • Deployment domain added to Firebase Authorized Domains
  • Registration works
  • Login works
  • Dashboard loads and shows data
  • R2-backed roost upload/version routes work
  • Real-time updates work
  • Custom domain configured and added to Firebase Authorized Domains, if applicable

troubleshooting

build fails

  • Wrong Node version: Select Node.js 22.x. The web build's engines pin (web/package.json) requires Node 22; Node 20 reproduces a next build failure.
  • Install fails: Use npm ci --legacy-peer-deps, matching web/nixpacks.toml.
  • Missing client env var: Verify every required NEXT_PUBLIC_FIREBASE_* variable is set for the build environment.
  • TypeScript errors: Run npm run build locally with the same Node and environment values.
  • Dependency issues: Ensure web/package-lock.json is committed.

app crashes after deploy

  • Check runtime logs in the host deployment logs.
  • Look for env var validation errors such as Firebase Admin SDK: Missing required environment variables.
  • Verify Firebase Admin variables: FIREBASE_PROJECT_ID, FIREBASE_CLIENT_EMAIL, FIREBASE_PRIVATE_KEY.
  • Verify session and encryption variables: SESSION_SECRET, MFA_ENCRYPTION_KEY, LLM_ENCRYPTION_KEY.
  • Verify R2 variables: R2_S3_ENDPOINT, R2_S3_ACCESS_KEY_ID, R2_S3_SECRET_ACCESS_KEY.
  • Verify CRON_SECRET, email, rate-limit, and Instatus variables when those routes or integrations are enabled.

auth not working

  • Add the deployment domain to Firebase Authorized Domains.
  • Verify NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN and NEXT_PUBLIC_BASE_URL match the target environment.
  • Clear browser cache and cookies.
  • Check browser console for Firebase error details.

roost uploads or version fetches fail

  • Verify the R2 credentials have object read and write access to the owlette buckets.
  • Verify R2_S3_ENDPOINT uses https://<account-id>.r2.cloudflarestorage.com.
  • Check ROOST_ENV, RAILWAY_ENVIRONMENT, and RAILWAY_PUBLIC_DOMAIN if writes are landing in the wrong dev/prod bucket.

slow performance

  • Cold starts: Use a plan that keeps the app warm for production.
  • Bundle size: Run npm run build and check .next/static output.
  • Firestore queries: Add indexes for frequently queried fields.

cost

railway pricing

Check the provider pricing page before provisioning. Keep development and production in separate projects or services so their usage, secrets, and billing limits can be managed independently.

optimization

  • Use separate plans or projects for development and production.
  • Optimize bundle size for faster cold starts.
  • Add a CDN such as Cloudflare for static assets and custom-domain traffic.

on this page