Agent-first observability. One Postgres instance. No humans required.
OwlMetry is a self-hosted observability platform designed for the agentic development era. Point your coding agent at the setup instructions, and it handles everything — integration, monitoring, debugging, performance analysis. The developer doesn't need to open a dashboard, configure alerts, or interpret charts. The agent does it all through the CLI.
Most observability tools are built for humans staring at dashboards. OwlMetry is built for agents making API calls. Every feature is accessible programmatically through agent API keys, a CLI, and a complete REST API. The web dashboard exists as an optional visual layer — not the primary interface.
Warning: This project is in active development and is not yet production-ready. APIs, schemas, and configuration may change without notice.
Traditional observability requires a human in the loop: someone to check dashboards, read alerts, interpret metrics, and decide what to fix. That made sense when humans wrote all the code. It doesn't make sense when your agent is already writing the code — it should also be the one monitoring it.
With OwlMetry, your agent can:
- Set up observability — create projects, register apps, and integrate the SDK into your codebase
- Monitor in production — query events, filter by level/app/time, investigate error clusters
- Diagnose issues — pull events around an incident window, correlate sessions, trace user journeys
- Act on what it finds — the agent reads the data, understands the problem, and writes the fix
The dashboard is there if you want to look. But you shouldn't have to.
Your analytics data is some of the most sensitive information you have — user behavior, device details, session traces, error logs. OwlMetry keeps all of it on your own infrastructure. No data leaves your servers, no third-party vendor has access, no privacy policy to hope they follow. This isn't a feature toggle — it's the architecture. Self-hosted by design means GDPR, HIPAA, and SOC 2 compliance becomes a property of your infrastructure, not a vendor promise.
And self-hosted doesn't have to mean complex. OwlMetry runs on a single Postgres instance. One database, one API server. That's the entire backend. Monthly partitioning handles event volume, auto-pruning manages disk space, and Postgres does what it's been doing reliably for decades.
- Agent-native API — every operation available through
owl_agent_keys: query events, list apps, read projects, analyze funnels. Agents are first-class citizens, not an afterthought - CLI for agents and humans —
--format jsonfor machine consumption,--format tablefor humans. Same tool, both audiences - Event ingestion — batch ingest up to 100 events per request with deduplication; supports gzip-compressed payloads
- Projects & apps — organize apps by product across platforms (
apple,android,web,backend); Apple platform covers iOS, iPadOS, and macOS with a single app - Device tracking — environment, OS version, app version, device model, locale, build number
- Anonymous identity — SDKs generate
owl_anon_IDs;/v1/identity/claimretroactively links anonymous events to a known user - Bundle ID validation — client API keys are scoped to an app's registered bundle ID, validated on every ingest request
- Funnel analytics — define conversion funnels and let your agent query drop-off rates programmatically
- Dashboard optional — Next.js web UI for when you want a visual overview. Not required for any workflow
- Single Postgres — no Kafka, no ClickHouse, no Redis. One database. Monthly partitioned events handle the scale
- Auth model —
owl_client_keys for SDKs,owl_agent_keys for agents/CLI, JWT for the optional dashboard. Role-based access: owner > admin > member - Team management — create teams, invite members by email, change roles, remove members
- Database auto-pruning — optional size limit (
MAX_DATABASE_SIZE_GB); drops oldest partitions first
apps/server Fastify API server (port 4000) — the core of OwlMetry
apps/cli CLI for agents and humans (agent key auth)
apps/web Next.js dashboard (port 3000) — optional visual layer
sdks/swift Swift SDK (Swift Package)
sdks/node Node.js Server SDK (zero runtime dependencies)
packages/shared Shared TypeScript types and constants
packages/db Drizzle ORM schema, migrations, seed
demos/ios iOS demo app
demos/node Node.js demo server
The API server is the product. Everything else — the dashboard, the CLI, the SDKs — is a client of that API. This means your agent has the same capabilities as the web UI. Nothing is dashboard-only.
- Node.js >= 20
- PostgreSQL >= 15
- pnpm
# Install dependencies
pnpm install
# Create database
createdb owlmetry
# Configure environment
cp .env.example .env
# Edit .env with your DATABASE_URL, JWT_SECRET, etc.
# Run migrations (creates tables + event partitions)
pnpm db:migrate
# Seed dev data (creates admin user, team, project, app, API keys)
pnpm db:seed
# Start the API server
pnpm dev:server
# Run tests (requires owlmetry_test database + Swift toolchain)
createdb owlmetry_test
pnpm test # Vitest + Swift SDK integration tests
pnpm test:swift-sdk # Swift SDK integration tests only
pnpm test:node-sdk # Node SDK integration tests only# Node.js 20+
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
# pnpm
corepack enable
corepack prepare pnpm@latest --activate
# PostgreSQL
sudo apt install -y postgresql postgresql-contrib
sudo systemctl enable postgresql
sudo systemctl start postgresql
# pm2 (process manager)
npm install -g pm2
# nginx
sudo apt install -y nginxsudo -u postgres createuser --superuser $(whoami)
createdb owlmetrygit clone <your-repo-url> /opt/owlmetry
cd /opt/owlmetry
pnpm install
pnpm build
cp .env.example .env
# Edit .env:
# DATABASE_URL=postgres://localhost:5432/owlmetry
# JWT_SECRET=<generate a random 64-char string>
# PORT=4000
# CORS_ORIGINS=https://your-domain.com
pnpm db:migrate
pnpm db:seedCreate ecosystem.config.cjs in the project root:
module.exports = {
apps: [
{
name: "owlmetry-api",
script: "apps/server/dist/index.js",
cwd: "/opt/owlmetry",
env: {
NODE_ENV: "production",
},
},
],
};pm2 start ecosystem.config.cjs
pm2 save
pm2 startup # follow the instructions to enable on boot# /etc/nginx/sites-available/owlmetry
server {
listen 80;
server_name api.your-domain.com;
location / {
proxy_pass http://127.0.0.1:4000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}sudo ln -s /etc/nginx/sites-available/owlmetry /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginxsudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d api.your-domain.comEvent partitions are auto-created on server startup (current month + 2 months ahead). If you want a safety net, add a monthly cron:
crontab -e
# Add: run migrations on the 1st of each month at midnight
0 0 1 * * cd /opt/owlmetry && pnpm db:migrate >> /var/log/owlmetry-partitions.log 2>&1To prevent the database from filling your disk, set MAX_DATABASE_SIZE_GB in .env. The server checks the total database size every hour (and once at startup). When the limit is exceeded, it drops the oldest monthly event partitions first. If only the current month remains and the database is still over the limit, it falls back to deleting the oldest individual event rows. Set to 0 (default) to disable.
MAX_DATABASE_SIZE_GB=10| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET |
/health |
None | Health check |
POST |
/v1/auth/register |
None | Create user + team |
POST |
/v1/auth/login |
None | Get JWT token + teams list |
GET |
/v1/auth/me |
JWT | Get current user profile + teams |
PATCH |
/v1/auth/me |
JWT | Update name or password |
GET |
/v1/auth/teams |
JWT | List user's teams |
GET |
/v1/auth/keys |
JWT | List API keys for user's teams |
GET |
/v1/auth/keys/:id |
JWT | Get single API key metadata |
POST |
/v1/auth/keys |
JWT (admin+) | Generate API key |
PATCH |
/v1/auth/keys/:id |
JWT (admin+) | Update API key name or permissions |
DELETE |
/v1/auth/keys/:id |
JWT (admin+) | Revoke an API key |
POST |
/v1/teams |
JWT | Create a new team |
GET |
/v1/teams/:id |
JWT | Get team details with members |
PATCH |
/v1/teams/:id |
JWT (admin+) | Rename team |
DELETE |
/v1/teams/:id |
JWT (owner) | Delete team |
GET |
/v1/teams/:id/members |
JWT | List team members |
POST |
/v1/teams/:id/members |
JWT (admin+) | Add member by email |
PATCH |
/v1/teams/:id/members/:userId |
JWT (admin+) | Change member role |
DELETE |
/v1/teams/:id/members/:userId |
JWT (admin+) | Remove member (or self-leave) |
POST |
/v1/ingest |
Client key | Batch ingest events |
GET |
/v1/events |
Agent key / JWT | Query events with filters |
GET |
/v1/events/:id |
Agent key / JWT | Get single event |
GET |
/v1/projects |
projects:read / JWT |
List projects |
GET |
/v1/projects/:id |
projects:read / JWT |
Get project with apps |
POST |
/v1/projects |
projects:write / JWT (admin+) |
Create project (requires team_id in body) |
PATCH |
/v1/projects/:id |
projects:write / JWT (admin+) |
Update project name |
DELETE |
/v1/projects/:id |
JWT only (admin+) | Soft-delete project and its apps |
GET |
/v1/apps |
apps:read / JWT |
List apps |
GET |
/v1/apps/:id |
apps:read / JWT |
Get single app |
POST |
/v1/apps |
apps:write / JWT (admin+) |
Create app (requires project_id) |
PATCH |
/v1/apps/:id |
apps:write / JWT (admin+) |
Update app name |
DELETE |
/v1/apps/:id |
JWT only (admin+) | Soft-delete app |
POST |
/v1/identity/claim |
Client key | Link anonymous events to a user ID |
The CLI is a thin HTTP client over the OwlMetry API. It works equally well as a tool for coding agents (--format json) and for humans (--format table).
# Build the CLI
pnpm build
# Configure endpoint and API key (saves to ~/.owlmetry/config.json)
node apps/cli/dist/index.js setup --endpoint http://localhost:4000 --api-key <agent-key>
# Or use environment variables
export OWLMETRY_ENDPOINT=http://localhost:4000
export OWLMETRY_API_KEY=<agent-key># An agent can do all of this programmatically
owlmetry projects # List projects
owlmetry projects view <id> # Project details with apps
owlmetry projects create --team-id <id> --name "My Project" --slug my-project
owlmetry apps # List apps
owlmetry apps --project <id> # Filter by project
owlmetry apps create --project <id> --name "iOS App" --platform apple --bundle-id com.example.app
owlmetry events --since 1h # Events from the last hour
owlmetry events --level error --app <id> # Errors for a specific app
owlmetry events view <id> # Full event details
owlmetry investigate <eventId> --window 10 # Events ±10 min around target--format json— machine-readable, ideal for agents--format table(default) — human-readable tables--format log— color-coded log lines (best for tailing events)
Zero-dependency server-side SDK. Your agent can add this to any Node.js project in seconds.
- Create a backend-platform app in OwlMetry (via dashboard, CLI, or API)
- Use the generated
owl_client_key
import { Owl } from '@owlmetry/node';
// Initialize at server startup
Owl.configure({
endpoint: 'https://your-owlmetry.com',
apiKey: 'owl_client_xxx',
serviceName: 'api-server',
appVersion: '1.0.0',
});
// Simple logging
Owl.info('User logged in', { route: '/auth/login' });
Owl.error('Payment failed', { error: err.message });
Owl.warn('Rate limit approaching', { endpoint: '/v1/ingest' });
Owl.debug('Cache miss', { key: 'user:123:profile' });
// Scoped logger with preset userId
const owl = Owl.withUser('user_123');
owl.info('Processing order');
owl.error('Payment failed', { error: err.message });
// Graceful shutdown (flushes all buffered events)
await Owl.shutdown();- Events are buffered in memory and flushed every 5 seconds or when 20 events accumulate
- Payloads over 512 bytes are gzip-compressed
- Failed requests are retried up to 5 times with exponential backoff
- All logging methods never throw — errors go to
console.errorwhendebug: true session_idis generated perconfigure()call, representing the server process lifetime
In short-lived environments (Firebase Cloud Functions, AWS Lambda), buffered events may be lost when the process freezes or terminates before the flush timer fires. Use Owl.wrapHandler() to automatically flush after each invocation:
import { onRequest } from 'firebase-functions/v2/https';
import { Owl } from '@owlmetry/node';
Owl.configure({ endpoint: 'https://...', apiKey: 'owl_client_...' });
export const myFunction = onRequest(
Owl.wrapHandler(async (req, res) => {
Owl.info('Function invoked', { path: req.path });
res.send('OK');
}),
);- Safety net: The SDK also registers a
beforeExithook that flushes any remaining events when the Node.js event loop drains, catching events that slip through withoutwrapHandler. - Use
wrapHandler, notshutdown()—shutdown()destroys the transport, which breaks warm container reuse.wrapHandleronly flushes, keeping the SDK ready for the next invocation.
Both demo apps send events to the same "Demo Project" in the dashboard, showing iOS and backend events side by side.
A zero-dependency HTTP server using the Node SDK. Demonstrates Owl.wrapHandler() for auto-flushing and Owl.withUser() for scoped logging.
# Requires: OwlMetry server running on port 4000 with seeded data
cd sdks/node && npx tsc # Build the Node SDK (if not already built)
pnpm dev:demo-node # Starts on port 4007Endpoints:
GET /health— health checkPOST /api/greet— success path (info events):{ "name": "Alice", "userId": "user-42" }POST /api/checkout— error path (warn + error events):{ "item": "Widget", "userId": "user-42" }
The iOS demo includes a "Backend Demo" section that calls the Node demo server. Set a User ID in the Identity section first so iOS and backend events share the same user.
# Requires: OwlMetry server (port 4000) + Node demo server (port 4007)
pnpm dev:server # Terminal 1
pnpm dev:demo-node # Terminal 2
# Build and run iOS demo on simulator (Terminal 3)| Variable | Default | Description |
|---|---|---|
DATABASE_URL |
postgres://localhost:5432/owlmetry |
PostgreSQL connection string |
JWT_SECRET |
dev-secret-change-me |
Secret for signing JWTs |
PORT |
4000 |
API server port |
HOST |
0.0.0.0 |
API server bind address |
CORS_ORIGINS |
http://localhost:3000 |
Comma-separated allowed origins |
MAX_DATABASE_SIZE_GB |
0 (disabled) |
Max database size before pruning old events |