Dravyn Sentinel, end to end
Application monitoring and security-event detection you self-host or point at Dravyn's own instance — request timing, error tracking, security-pattern flagging, and alerting, with SDKs for Node and Python.
What Dravyn Sentinel is
A small SDK in your app, an ingestion API that stores everything it sends, and a dashboard that turns that stream into request-rate, error-rate, and latency charts — plus automatic flagging of SQL-injection, XSS, and brute-force patterns.
Instead of wiring up separate error tracking, APM, and log search tools, one SDK call captures all three: capture() for arbitrary log events, an auto-instrumented middleware for request timing, and uncaught-exception hooks for errors. Every event lands in the same store and is searchable from the same dashboard.
It's not a wrapper around a third-party SaaS. The ingestion API is a plain FastAPI service backed by your own ClickHouse instance (or SQLite for local dev), with Resend for alert email and an optional Slack webhook — both swappable via environment variables.
Core concepts
- Event
- The one row shape everything becomes:
request,error,security, orlog. Always carries aproject_id, atype, and a timestamp; other fields depend on the type. - Project
- A string ID you choose (
"my-app") that every event is scoped to. There's no project-registration step — the first event ingested for a newproject_idjust starts showing up. - API key
- A bearer token in
SENTINEL_API_KEYS(comma-separated on the server). Any valid key can read or write any project — there's no per-key project scoping yet. - Security event
- A
requestorlogevent that the rule engine re-tagged astype: "security"after matching a SQLi/XSS pattern or a brute-force login streak. - Stats window
- A one-minute bucket of aggregated request count, error count, and p50/p95/p99 latency — what the dashboard's charts are built from.
- Alert cooldown
- Once an error-rate alert fires for a project, it won't fire again for that project for 10 minutes, even if the error rate stays high.
Architecture & repo layout
Three moving pieces, one event schema flowing through all of them: your app + SDK → ingestion API (FastAPI, :8000) → event store (ClickHouse, Postgres, or SQLite) → dashboard (Next.js, :3000).
Stack per layer
| Layer | Choice | Why |
|---|---|---|
| Ingestion API | FastAPI | Async, typed request/response models, auto-generated OpenAPI |
| Event store (scale) | ClickHouse | Columnar store built for high-volume append-only event data |
| Event store (free-tier prod) | Postgres (Neon) | Real persistence with no paid infra — events survive Render free-plan spin-downs/redeploys; swap in via SENTINEL_STORE=postgres |
| Event store (dev) | SQLite | Zero-infra local iteration — same EventStore interface, swap via SENTINEL_STORE |
| Alert email | Resend | Same provider Dravyn Auth and Dravyn Web use |
| Dashboard | Next.js 14 + Recharts | Server routes keep the API key off the client; polling-based charts |
Feature matrix
What's actually running today versus what still needs rolling-baseline anomaly detection or WebSocket push to feel complete.
Shipped
Partial / not built
The SQLi/XSS regexes are intentionally broad and will produce false positives on legitimate text. They tag and alert — they never block a request. Tune _SQLI_PATTERN / _XSS_PATTERN in security_rules.py per project before treating this as a WAF.
Getting started
Full stack (Docker)
cd "Dravyn Sentinel"
docker compose up --build
# dashboard: http://localhost:3000
# ingestion: http://localhost:8000
Starts ClickHouse, the ingestion API, and the dashboard together — reads a .env file next to docker-compose.yml automatically.
Ingestion API only, no Docker
cd ingestion
python3.12 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
uvicorn app.main:app --reload --port 8000
Falls back to a local SQLite file (dev/test only) and loads ingestion/.env automatically via python-dotenv.
Use Python ≤ 3.13. Python 3.14 fails to build pydantic-core from source — PyO3 doesn't support 3.14 yet and no prebuilt wheel exists. Install 3.12 with brew install python@3.12 if needed.
Self-serve API keys
No admin required — generate your own scoped key from the dashboard.
The dashboard logs you in via Dravyn Auth on any page you visit (Sentinel is registered as its own Dravyn Auth project, separate from Dravyn Auth's own accounts). Head to /keys once logged in, create a project, and generate a key for it in a few clicks.
- Open
/keys, create an account (first/last name, email + password with a strength meter, OTP-verified) or log in - Name a project and pick a platform (Node.js, Python, or Other/REST) — the name is slugified into the
project_idyour SDK will use, e.g. "My App" →my-app; the platform just decides which quickstart snippet you're shown next, it doesn't restrict what actually calls the ingest API - Click Generate key — the raw key is shown once, alongside a ready-to-paste quickstart snippet for the platform you picked
Signup can ask for more than name, email, and password — Dravyn Auth projects (including this one) can define extra registration fields from its portal. If Sentinel's project has any defined, they render automatically on this form via getCustomFields()/register(email, password, profile, customFields), and the values come back on the account like any other profile data.
A self-serve key only works for the one project it was issued for — sending events tagged with a different project_id returns 403. This is different from the legacy SENTINEL_API_KEYS env-var allowlist (still used internally by Dravyn's own products), where any key can touch any project.
Projects & keys are backed by Postgres (ACCOUNTS_DATABASE_URL), and raw event/log data is too (SENTINEL_STORE=postgres, SENTINEL_DATABASE_URL — defaults to the same database as ACCOUNTS_DATABASE_URL unless set separately). Nothing here still lives in a local file on the free plan, so none of it is wiped by Render's spin-downs or redeploys — see §15.
API reference
Every endpoint below requires Authorization: Bearer <api-key> except /healthz. Base URL defaults to http://localhost:8000 locally, https://dravyn-sentinel-ingestion.onrender.com in production (see §15 — no custom domain yet).
POST /v1/events
Ingest a batch of events. Runs security-rule classification and the alert-threshold check before returning.
{
"events": [
{ "project_id": "demo", "type": "request", "method": "GET",
"route": "/users", "status_code": 200, "duration_ms": 42 }
]
}
-> 200 { "data": { "accepted": 1, "flagged_security_events": 0 }, "meta": {} }
GET /v1/stats?project_id=&since_minutes=
Per-minute aggregates: request count, error count, p50/p95/p99 latency. since_minutes defaults to 60, capped at 1440.
GET /v1/logs?project_id=&level=&service=&user_id=&q=&since_minutes=&limit=
Search recent events, newest first. level/service/user_id are exact match, q is a substring match on message. limit defaults to 200, capped at 1000.
GET /healthz
No auth required. Returns {"status": "ok"} — use for container/load-balancer health checks.
Self-serve projects & keys — /v1/projects
A different auth model from everything else on this page: these take Authorization: Bearer <dravyn-auth-session-token>, not a Sentinel API key — this is the "logged into the dashboard" surface (see §06), not something your SDK calls.
| Route | Body / params | Returns |
|---|---|---|
POST /v1/projects | name, platform — platform is "node" | "python" | "other" | project, 404 if the slugified name already exists |
GET /v1/projects | — | projects owned by the calling Dravyn Auth user |
POST /v1/projects/:id/keys | — | { api_key, ...record } — raw key shown once, never again |
GET /v1/projects/:id/keys | — | keys (prefix only) |
DELETE /v1/projects/:id/keys/:keyId | — | { revoked: true } |
Rate limits & errors
6,000 requests per rolling 60-second window, per API key. Exceeding it returns 429 with a Retry-After header. Every response follows { "data": ..., "meta": {} } on success, { "detail": ... } on error.
Node.js SDK — @dravyn/sentinel
import { init } from "@dravyn/sentinel";
init({
apiKey: process.env.SENTINEL_API_KEY!,
projectId: "my-app",
ingestUrl: "https://dravyn-sentinel-ingestion.onrender.com", // or http://localhost:8000
service: "api",
captureUncaught: true, // hooks process uncaughtException / unhandledRejection
});
import { sentinelMiddleware, sentinelErrorHandler } from "@dravyn/sentinel/express";
app.use(sentinelMiddleware()); // records one `request` event per response
// ...your routes...
app.use(sentinelErrorHandler()); // captures thrown errors before your handler
import { capture, captureError } from "@dravyn/sentinel";
try {
await chargeCard(order);
} catch (err) {
captureError(err, { orderId: order.id });
throw err;
}
Delivery is best-effort and non-blocking — if the ingestion API is unreachable, flush() swallows the error rather than crashing your app.
Python SDK — dravyn-sentinel
import dravyn_sentinel as sentinel
sentinel.init(
api_key=os.environ["SENTINEL_API_KEY"],
project_id="my-app",
ingest_url="https://dravyn-sentinel-ingestion.onrender.com",
service="api",
)
from dravyn_sentinel.fastapi import SentinelMiddleware
app.add_middleware(SentinelMiddleware) # captures requests + unhandled exceptions
from dravyn_sentinel import capture, capture_exception
try:
process_payout(batch)
except Exception as exc:
capture_exception(exc, {"batch_id": batch.id})
raise
The client flushes on a background daemon thread and registers an atexit hook — a final flush runs on normal interpreter shutdown, but not on SIGKILL or a hard crash.
Security event detection
Every ingested request or log event is scanned before storage. A match re-tags the event type: "security" and bumps its level to at least warn.
| Category | Trigger |
|---|---|
sql_injection | Regex match for UNION SELECT, OR 1=1, DROP TABLE, trailing SQL comments, tautology quotes |
xss | Regex match for <script>, onerror=, javascript: |
brute_force | ≥5 401 responses for the same user_id within a 5-minute rolling window |
Find flagged events with GET /v1/logs?project_id=…&level=warn, or filter the dashboard's Logs page by level and look at the Type column.
Alerting
After every ingest call, a background task checks the last 5 minutes of stats for that project. If the error rate crosses SENTINEL_ERROR_RATE_THRESHOLD (default 5%) with at least SENTINEL_ALERT_MIN_SAMPLES requests (default 20), it fires once via Resend email and/or a Slack webhook, then goes quiet for a 10-minute cooldown.
| Variable | Meaning |
|---|---|
RESEND_API_KEY | Required for email delivery |
SENTINEL_ALERT_EMAIL | Recipient address |
SENTINEL_ALERT_EMAIL_FROM | Sender — must be a verified Resend domain |
SENTINEL_ALERT_SLACK_WEBHOOK | Incoming webhook URL; unset skips Slack |
A misconfigured RESEND_API_KEY won't crash ingestion — it just won't deliver. Confirm SENTINEL_ALERT_EMAIL is set before assuming alerting is live.
Uptime monitoring
A background loop pings every URL in SENTINEL_UPTIME_TARGETS (comma-separated project_id=url pairs) on an interval and records the result as a log event with service: "uptime-monitor" — no separate status-page UI, it shows up in the same stats/search surface as everything else.
SENTINEL_UPTIME_TARGETS=dravyn-web=https://dravyn.it.com,dravyn-auth=https://auth.dravyn.it.com
SENTINEL_UPTIME_INTERVAL_SECONDS=60
Dashboard
A Next.js app at apps/dashboard, entirely gated behind Dravyn Auth login — every page, not just §06's key management. There's no server-side admin key or hardcoded default project anymore; after logging in you see only your own projects, picked from a switcher in the nav, and every request goes straight from your browser to Dravyn Auth or the ingestion API using your own session.
- Overview (
/) — stat tiles, requests-vs-errors chart, latency-percentile chart for the selected project, polling every 5s. - Logs (
/logs) — free-text search, level filter, table with time/level/type/service/message/route/status/duration, polling every 5s. Which project you're looking at is an explicit selector at the top of the page (not just the one in the nav bar) — the heading names it too, so it's never ambiguous which project's logs are on screen. - Projects & Keys (
/keys) — create projects (name + platform), generate/revoke API keys, see a platform-specific quickstart snippet right after generating a key.
Viewing your own stats and logs needs no API key at all — /v1/stats and /v1/logs accept your Dravyn Auth session directly, checked against project ownership. A raw key is only for your own SDK integration code.
This used to be a single unauthenticated project view (SENTINEL_DEFAULT_PROJECT_ID) with a bolted-on authenticated key-management page. The whole dashboard is gated now — see the "Recently shipped" callout in §15.
Deploying
| Piece | Where |
|---|---|
| Ingestion API | Deployed via render.yaml, live at dravyn-sentinel-ingestion.onrender.com (free plan) |
| ClickHouse | Managed instance or self-hosted — one node is enough at MVP scale |
| Dashboard | apps/dashboard/Dockerfile, or Vercel like the other Dravyn Next.js apps |
Lock allow_origins in main.py down to the dashboard's real origin before production — it's wide open by default for local dev. Move the rate limiter and alert cooldown to Redis before running more than one ingestion replica; both hold state in-process today.
What's left
Everything in the feature matrix marked partial or not built, plus:
- A custom domain for the ingestion API — it's live on Render today at
dravyn-sentinel-ingestion.onrender.com;sentinel.dravyn.it.comended up pointed at the dashboard instead, so the API still needs its own hostname (a subdomain likeingest.sentinel.dravyn.it.comCNAME'd at it). - ClickHouse for real scale — the Postgres event store (
SENTINEL_STORE=postgres) is real persistence, but it's a single table with bucketing/percentiles computed in Python, not a columnar store with query-time aggregation. Fine for self-serve/early-stage volume; switch toSENTINEL_STORE=clickhouseonce that stops being true.
Both SDKs are published (npm install @dravyn/sentinel, pip install dravyn-sentinel), self-serve API keys enforce per-project scoping, the entire dashboard requires Dravyn Auth login and shows only your own projects and data, and everything — self-serve projects/keys and raw event/log data alike — now persists on free-tier Postgres instead of a wipeable local file. Nothing on the free plan is lost to spin-down or redeploy anymore.