A self-hostable observability platform for runtime errors, service health, and deployments: a single-developer answer to "where did this break, and why?"
Live demo: https://runtimecatch.up.railway.app
Built with Next.js 16 (App Router, Server Components, Server Actions), Prisma 7 + PostgreSQL 16, TypeScript, Tailwind v4, and Zod. Auth is credentials-based; ingestion is project-scoped behind hashed API keys. No third-party auth or telemetry SDKs, just node:crypto.
Sign in to the live demo with:
- Email:
owner@runtimecatch.dev - Password:
runtimecatch
All demo data is seeded and fake. Services, issues, deployments, and events are generated by prisma/seed.ts and a background simulator. Nothing in the demo represents real traffic.
- Credentials auth: passwords hashed with scrypt (
node:crypto), opaque httpOnly session cookies,Securein production. - Project-scoped API keys: generated once, stored only as SHA-256 hashes; the plaintext is never reachable from the database.
- Bearer-protected ingestion:
POST /api/eventsauthenticates every request against a key's project before accepting an event. - Fingerprint-based issue grouping: events collapse into issues by a deterministic fingerprint, so recurring errors increment an occurrence count instead of flooding the feed.
- Prisma / PostgreSQL schema: a relational model for projects, services, issues, alerts, deployments, and runtime events.
- Docker local development: web + Postgres run in Compose, isolated from any other local stack.
- TypeScript SDK and CLI: a typed client (
@runtimecatch/client) and a dependency-free CLI for wiring services in and firing test events.
SDK / CLI -> POST /api/events -> PostgreSQL -> Next.js dashboard
(your apps) (Bearer-authed) (via Prisma) (Server Components)
- A Next.js dashboard (App Router + Server Components) renders services, issues, and metrics.
- PostgreSQL + Prisma form the data layer for every entity in the system.
- The
/api/eventsingestion endpoint authenticates a project API key, validates the payload with Zod, and persists a runtime event. - The TypeScript SDK and CLI send events to that endpoint from any application.
- On ingestion, each event is fingerprinted and grouped into an issue; the same data feeds the dashboard's charts and per-service health.
Install the local client (@runtimecatch/client), configure it once at module load, then capture from anywhere:
// lib/runtimecatch.ts
import "server-only";
import { configureRuntimeCatch } from "@runtimecatch/client";
configureRuntimeCatch({
apiUrl: process.env.RUNTIMECATCH_API_URL!, // e.g. https://your-instance.up.railway.app
apiKey: process.env.RUNTIMECATCH_API_KEY!, // project key from /settings/api-keys
service: process.env.RUNTIMECATCH_SERVICE!, // must exist in the key's project
environment: process.env.RUNTIMECATCH_ENVIRONMENT ?? "development",
});
export { captureEvent, captureException } from "@runtimecatch/client";// report a structured event from a server action
await captureEvent("post.published", { title, userId: "usr_123" });
// report a caught exception from a route handler
try {
// ...
} catch (err) {
await captureException(err, { route: "GET /api/widgets" });
}A copy-paste-ready example lives in examples/nextjs-app/. The SDK isn't on npm yet: vendor packages/runtimecatch-client/dist/ or install via a local path (npm install file:...); the example walks through both.
Clone to a live dashboard with streaming events in about five minutes:
git clone https://github.com/bconti123/RuntimeCatch.git && cd RuntimeCatch
npm install
cp .env.example .env
npm run db:up && npm run db:migrate && npm run db:seed
npm run devThen open localhost:3000, sign in with the demo credentials above, and run npm run simulate in another terminal to stream realistic events. The seeded demo API key (local dev only) is:
rc_live_demo_key_for_local_testing_only
Send an event to confirm the ingestion path:
curl -X POST http://localhost:3000/api/events \
-H "Authorization: Bearer rc_live_demo_key_for_local_testing_only" \
-H "Content-Type: application/json" \
-d '{
"service": "playback-service",
"severity": "CRITICAL",
"category": "PLAYBACK_ERROR",
"message": "DRM license timeout",
"metadata": { "region": "us-west", "device": "smart-tv" }
}'Drop the Authorization header and the same request returns 401.
Web + Postgres run together in Compose, isolated from any other local stack:
cp .env.example .env # host ports + database config live here
docker compose up --build
# once the web container is healthy, in another terminal:
docker compose exec web npx prisma migrate dev
docker compose exec web npm run db:seedThen visit localhost:3000 and sign in. Host ports are configurable in .env (WEB_PORT, POSTGRES_HOST_PORT) without affecting anything inside the Compose network. The web service uses a Docker-internal DATABASE_URL (the postgres service name) that overrides the host value from .env, so switching between npm run dev and docker compose up needs no edits.
docker compose logs -f web # tail dev server output
docker compose down # stop (data persists)
docker compose down -v # stop and wipe the databaseGitHub Actions (.github/workflows/ci.yml) runs on every push and pull request and must pass before changes ship. It installs with npm ci, generates the Prisma client, then runs npm run lint, npm run typecheck, and npm run build — so a broken lint, type error, or build failure is caught in CI rather than after it's live. The standalone examples/ app is a copy-paste SDK sample, not part of the deployed build, and is excluded from both lint and typecheck.
Railway auto-deploys from main: a web service plus a managed PostgreSQL service. A merge to main triggers a deploy once CI is green.
A railway.json builds with Nixpacks (the local-only Dockerfile.dev is never used by Railway) and starts with npm run start:prod, which runs prisma migrate deploy immediately before next start. Migrations therefore apply on every deploy, with no separate release step. The production database must be Railway's managed Postgres (${{Postgres.DATABASE_URL}}) — never a localhost URL.
Required environment variables:
| Variable | Notes |
|---|---|
DATABASE_URL |
Reference the Postgres service as ${{Postgres.DATABASE_URL}}. |
SESSION_SECRET |
Secret for session/CSRF signing; generate with openssl rand -hex 32. |
NODE_ENV |
Set to production (Railway does this), enabling Secure cookies and quieter Prisma logs. |
PORT is injected by Railway and bound automatically by next start. To deploy: create a Railway project from this GitHub repo, add a PostgreSQL service, set the variables above, and deploy. Seed demo data once with npm run db:seed (via Run a command or railway run), then generate a public domain.
Authenticated with Authorization: Bearer <project-api-key>.
| Field | Type | Required |
|---|---|---|
service |
string | yes, must exist in the API key's project |
severity |
enum | yes: DEBUG | INFO | WARNING | ERROR | CRITICAL |
category |
enum | yes: RUNTIME_ERROR | API_LATENCY | PLAYBACK_ERROR | DEPLOYMENT | DATABASE | NETWORK | AUTH | APP_EVENT |
message |
string | yes, up to 2,000 chars |
stackTrace |
string | no, up to 20,000 chars |
metadata |
object | no, arbitrary JSON |
fingerprint |
string | no, overrides the auto-computed value |
occurredAt |
ISO 8601 | no, defaults to now() |
Responses: 201 with { eventId, issueId, fingerprint, occurrenceCount }, 400 on validation failure, 401 on a missing/invalid/revoked key, 404 if the service isn't in the key's project.
A dependency-free developer CLI for wiring a service in and firing test events without hand-writing curl. Run npm link once to put it on your PATH, or use npm run cli -- <command>.
runtimecatch init # create / update .runtimecatchrc.json
runtimecatch env # print current config (API key masked)
runtimecatch test # send a simple INFO event to /api/events
runtimecatch send-error # send a sample ERROR event with stackTrace + metadata| Command | Purpose |
|---|---|
npm run dev |
Next.js dev server |
npm run build |
Production build |
npm run db:up / db:down |
Start / stop the Postgres container |
npm run db:migrate |
Apply Prisma migrations |
npm run db:reset |
Drop, migrate, reseed in one shot |
npm run db:seed |
Run prisma/seed.ts |
npm run db:studio |
Open Prisma Studio |
npm run simulate |
Stream realistic events into the DB |
npm run cli -- <cmd> |
Run the runtimecatch CLI without a global link |
Ordered roughly by what's most worth doing next given the current codebase:
- Saved views + full-text search:
tsvectorindexes on issues; Postgres can carry this without a separate search service. - Slack / webhook notifications on alert transitions: route
Alert.statuschanges through an outbox table so retries and dedupe are explicit. - Per-IP rate limiting on
/loginand/api/events, plus CSRF tokens on the logout form. - Multi-tenant teams + org roles: the schema already centers on
Project.ownerId; addingMembership(userId, projectId, role)is mostly a join. - Source-map symbolication for stack traces, run as a background job after ingestion (the point where a queue starts to earn its place).
- Argon2id in place of scrypt once the auth surface stabilizes;
lib/auth.tsis already shaped for the swap.
MIT. See LICENSE.



