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.
| source | baseline |
|---|---|
web/package.json engines | node 22.x |
Repository root package.json | node >=20.9.0, npm >=10.0.0 |
Repository .nvmrc | 20.18.0 |
web/nixpacks.toml | nodejs_20, npm-10_x |
| Firebase Functions | Node 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).
railway deployment (recommended)
step 1: create railway project
- Go to railway.app and sign up with GitHub.
- Click New Project -> Deploy from GitHub repo.
- Select the owlette repository.
step 2: configure service
- Click your service -> Settings.
- Set Root Directory to
web. - Set Branch to
mainfor production ordevfor development. - 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 theINSTATUS_COMPONENT_*_IDvalues when/api/cron/status-pingshould publish to Instatus - Autonomous Cortex:
CORTEX_INTERNAL_SECRETwhen 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
- Copy your Railway deployment URL.
- Go to Firebase Console -> Authentication -> Settings -> Authorized Domains.
- 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)
- Railway -> Settings -> Networking -> Add Custom Domain.
- Add the CNAME or A record as instructed.
- Railway provisions SSL automatically.
- Add the custom domain to Firebase Authorized Domains.
- Update
NEXT_PUBLIC_BASE_URLto the custom HTTPS origin and redeploy.
vercel deployment
Vercel can run the web app as a standard Next.js project.
step 1: import project
- In Vercel, click Add New -> Project.
- Import the owlette repository.
- Set Root Directory to
web. - Set the framework preset to Next.js.
step 2: configure build settings
Use the same commands as the checked-in Railway/Nixpacks deployment:
| setting | value |
|---|---|
| Install Command | npm ci --legacy-peer-deps |
| Build Command | npm run build |
| Output Directory | Next.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:
| branch | deployment | url |
|---|---|---|
dev | Development | dev.owlette.app |
main | Production | owlette.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 startRequirements:
- Node.js 22.x, npm 10.x
- All required environment variables from Environment Variables
- A writable port supplied as
PORTor 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-depssucceeds -
npm run buildsucceeds - Required environment variables are set for the target environment
-
NEXT_PUBLIC_BASE_URLmatches 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 startwhere the host requires one -
CRON_SECRETconfigured - 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
enginespin (web/package.json) requires Node 22; Node 20 reproduces anext buildfailure. - Install fails: Use
npm ci --legacy-peer-deps, matchingweb/nixpacks.toml. - Missing client env var: Verify every required
NEXT_PUBLIC_FIREBASE_*variable is set for the build environment. - TypeScript errors: Run
npm run buildlocally with the same Node and environment values. - Dependency issues: Ensure
web/package-lock.jsonis 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_DOMAINandNEXT_PUBLIC_BASE_URLmatch 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_ENDPOINTuseshttps://<account-id>.r2.cloudflarestorage.com. - Check
ROOST_ENV,RAILWAY_ENVIRONMENT, andRAILWAY_PUBLIC_DOMAINif 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 buildand check.next/staticoutput. - 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.