Owlmetry
Self-Hosting

Configuration

Environment variables, database connection, email (Resend), cookie domain, and optional settings.

Environment variables and configuration options. All values are set in the .env file at the repository root.

The API server auto-loads .env via dotenv on startup. The web dashboard reads environment variables at build time (Next.js) and runtime (standalone mode).

Required variables

VariableDescription
DATABASE_URLPostgreSQL connection string. Format: postgresql://user:password@host:5432/dbname. The setup script prints this value after creating the database.
JWT_SECRETRandom string used to sign JWT tokens. Generate with openssl rand -base64 48. Must be kept secret. Changing this value invalidates all existing sessions.
CORS_ORIGINSAllowed origin for cross-origin requests from the web dashboard. Set to your dashboard URL (e.g., https://yourdomain.com).
COOKIE_DOMAINCookie domain for cross-subdomain authentication. Set to .yourdomain.com (note the leading dot) so the auth cookie is shared between yourdomain.com and api.yourdomain.com. Required when the API and dashboard are on different subdomains.
OWLMETRY_ATTACHMENTS_SIGNING_SECRETRequired in production. Random string used to sign attachment download URLs. Generate with openssl rand -hex 32. Must differ from JWT_SECRET. Outside production, falls back to JWT_SECRET so local dev just works.

Optional variables

VariableDefaultDescription
PORT4000Port the API server listens on.
HOST0.0.0.0Bind address for the API server.
MAX_DATABASE_SIZE_GB0 (disabled)When set to a positive number, enables automatic pruning. An hourly check (plus one on startup) drops the oldest monthly event partitions when the database exceeds this size.
RESEND_API_KEY(empty)API key from Resend for sending verification emails. If not set, verification codes are printed to the server console (useful for single-user or development setups).
EMAIL_FROM[email protected]The "from" address used when sending verification and invitation emails via Resend.
NODE_ENVdevelopmentSet to production for production deployments. Affects cookie security (secure flag) and Next.js behavior.
WEB_APP_URLhttp://localhost:3000Base URL of the web dashboard. Used for links in invitation emails and redirects. Set to your dashboard URL in production (e.g., https://yourdomain.com).
API_PUBLIC_URLhttps://api.owlmetry.comPublic base URL of the API server. Included in attachment upload/download URLs returned to SDKs and the dashboard. Set to your API subdomain when self-hosting.
SYSTEM_JOBS_ALERT_EMAIL(empty)If set, system job runs send a completion/failure email to this address. Leave empty to disable system-job alerts.
OWLMETRY_ATTACHMENTS_PATH./data/attachments (dev) / /opt/owlmetry-attachments (prod)Directory on disk where event attachment bytes are stored. Must be writable by the API server process. Exclude from pg_dump backups — attachments live on disk, not in Postgres.
OWLMETRY_ATTACHMENTS_INTERNAL_URI(empty)Optional internal URI prefix used by nginx X-Accel-Redirect when serving attachment downloads. See deploy/attachments-nginx-snippet.conf.
APNS_KEY_ID(empty)Apple .p8 key id (10-char) from Apple Developer → Keys. Required to enable the iOS branch of mobile push deliveries.
APNS_TEAM_ID(empty)Apple Developer team id (10-char).
APNS_KEY_P8(empty)Full PEM contents of the .p8 key downloaded from Apple Developer. Wrap in double quotes in .env — dotenv 16 preserves real newlines inside quoted values. Treat as a secret.
APNS_BUNDLE_ID(empty)Bundle id of the iOS app (e.g., com.owlmetry.dashboard). Sent as apns-topic on every push.

Example .env file

NODE_ENV=production
DATABASE_URL=postgresql://owlmetry:yourpassword@localhost:5432/owlmetry
JWT_SECRET=your-random-64-char-secret-here
PORT=4000
HOST=0.0.0.0
CORS_ORIGINS=https://yourdomain.com
WEB_APP_URL=https://yourdomain.com
API_PUBLIC_URL=https://api.yourdomain.com
COOKIE_DOMAIN=.yourdomain.com
MAX_DATABASE_SIZE_GB=8
RESEND_API_KEY=re_abc123...
EMAIL_FROM=[email protected]
SYSTEM_JOBS_ALERT_EMAIL=[email protected]

# Event attachments
OWLMETRY_ATTACHMENTS_PATH=/opt/owlmetry-attachments
OWLMETRY_ATTACHMENTS_SIGNING_SECRET=your-random-hex-string-32-bytes
OWLMETRY_ATTACHMENTS_INTERNAL_URI=/_attachments/

# Mobile push — iOS via APNs (optional; leave unset to disable iOS push deliveries)
APNS_KEY_ID=ABC1234567
APNS_TEAM_ID=XYZ7654321
APNS_KEY_P8="-----BEGIN PRIVATE KEY-----
...multi-line PEM contents...
-----END PRIVATE KEY-----"
APNS_BUNDLE_ID=com.yourdomain.app

Migration note

Database migrations (pnpm db:migrate) run from the packages/db directory and do not auto-load the .env file. When running migrations over SSH, prefix the command with the DATABASE_URL:

DATABASE_URL='postgresql://owlmetry:password@localhost:5432/owlmetry' pnpm db:migrate

The API server loads .env automatically via its config module, so this is only relevant for running migrations in isolation (e.g., during deployment scripts).

The COOKIE_DOMAIN variable controls the domain attribute of the token cookie:

  • Set to .yourdomain.com when the dashboard (yourdomain.com) and API (api.yourdomain.com) are on different subdomains. The leading dot allows the cookie to be shared across subdomains.
  • Leave empty for local development where everything runs on localhost.

The auth cookie is always httpOnly, sameSite: lax, and secure in production (NODE_ENV=production). Its maxAge is set to 10 years so dashboard sessions don't expire — the JWT itself has no expiry claim.

Database size management

When MAX_DATABASE_SIZE_GB is set to a positive value:

  1. On server startup, a size check runs (fire-and-forget, does not block startup).
  2. An hourly interval checks the database size.
  3. If the database exceeds the threshold, the oldest monthly partitions are dropped from events, metric_events, and funnel_events tables.
  4. If only the current month's partition remains and the database is still over the limit, row-level deletion is used as a fallback.

Set this to a value safely below your disk capacity (e.g., 8 for a 25 GB disk) to prevent the server from running out of storage.

Mobile push (APNs)

The mobile_push channel routes per device by user_devices.platform. iOS rows go through APNs using token-based auth with an Apple .p8 key — the same ES256 / EC P-256 pattern used by App Store Connect API, Apple Search Ads, and Sign in with Apple. One Apple Developer account → one set of env vars covers the whole server. Android rows are reserved for the FCM transport that will land later.

VariableDescription
APNS_KEY_IDThe 10-char key id printed next to the key on the Apple Developer Keys page.
APNS_TEAM_IDYour Apple Developer 10-char team id.
APNS_KEY_P8Full PEM contents of the .p8 file. Wrap in double quotes in .env — dotenv 16 preserves real newlines inside quoted values, so paste the file verbatim. Treat as a secret.
APNS_BUNDLE_IDBundle id of the iOS app — sent as the apns-topic header on every push.

The server auto-routes each push to api.sandbox.push.apple.com or api.push.apple.com based on the per-device value the iOS client reports at registration time (read from aps-environment in the embedded provisioning profile). One server can serve Debug installs, TestFlight, and App Store builds side-by-side — no env flag, no manual flip.

These are all optional. If APNS_KEY_ID, APNS_TEAM_ID, APNS_KEY_P8, or APNS_BUNDLE_ID is missing the mobile_push adapter is omitted at boot and every iOS push delivery is silently marked skipped — local dev and self-hosted setups without iOS push setup keep working. Inbox and email channels are unaffected.

The server hard-deletes a user_devices row when APNs returns 410 Unregistered or 400 BadDeviceToken, so stale tokens self-clean on the next push attempt.

See Notifications for the dispatcher and channel model.

Ready to get started?

Connect your agent via MCP or CLI and start tracking.