Environment configuration
The file bridge can't write .env files, so copy these by hand.
Append to your dev .env
# โโโ Telemetry store โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# postgres (default, dev) | clickhouse (production / SaaS)
TELEMETRY_STORE=postgres
CLICKHOUSE_URL=http://localhost:8123
CLICKHOUSE_DB=sentinel
CLICKHOUSE_USER=default
CLICKHOUSE_PASSWORD=
# Optional read replica for dashboard queries (managed PG providers hand these out)
# DATABASE_REPLICA_URL=
# โโโ Retention โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
RETENTION_LOGS_DAYS=30
RETENTION_METRICS_DAYS=400
Create .env.production from this template
POSTGRES_DB=sentinel
POSTGRES_USER=sentinel
POSTGRES_PASSWORD=change-me
CLICKHOUSE_DB=sentinel
CLICKHOUSE_USER=sentinel
CLICKHOUSE_PASSWORD=change-me
# Public URL the browser uses to reach the API
PUBLIC_API_URL=https://api.sentrinel.example.com
# Optional managed-Postgres read replica
DATABASE_REPLICA_URL=
RETENTION_LOGS_DAYS=30
RETENTION_METRICS_DAYS=400
Trying the ClickHouse path locally
docker compose --profile ch up -d # Postgres + ClickHouse
bun run --cwd packages/db ch:migrate # apply ClickHouse schema
# set TELEMETRY_STORE=clickhouse in .env, then:
bun run dev
curl localhost:3001/health # โ telemetryStore: "clickhouse"
New in the completed roadmap
# โโโ Authentication & multi-tenancy โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# Self-serve signup is on by default and gives each new account its own
# organization. Set this to lock a private deployment to invite-only โ the very
# first account can always be created, or there'd be no way in.
# SENTRINEL_DISABLE_SIGNUP=true
Email notifications
Email is the one channel a tenant cannot configure alone: sending mail needs a credential, and that belongs to whoever runs the deployment. Set these and "Email" becomes selectable when adding a notification channel; leave them unset and the dashboard refuses to create one, naming the missing setting rather than accepting a channel that would silently deliver nothing.
# From: address. Required โ there is no sane default, and providers reject mail
# from a domain you have not verified with them.
[email protected]
# resend (default) | generic
# SENTRINEL_EMAIL_PROVIDER=resend
SENTRINEL_EMAIL_API_KEY=re_โฆ
# "generic" instead POSTs {from,to,subject,text,html} to a URL you control โ
# a relay, an n8n flow, a Lambda in front of SES. Use this for SMTP: put a
# relay in front of it rather than expecting Sentrinel to speak SMTP.
# SENTRINEL_EMAIL_PROVIDER=generic
# SENTRINEL_EMAIL_URL=https://relay.internal/send
What SENTRINEL_REQUIRE_AUTH=true changes
| off (default) | on | |
|---|---|---|
| Dashboard API | open | 401 without a session |
?appId= from another org |
served | 403 |
| Unscoped lists (apps, issues, alerts, keysโฆ) | everything | only your org's |
| Live tail (SSE) | all traffic | only your org's |
| SQL console | unrestricted | rows filtered to your apps (project app_id) |
Tenant isolation is enforced in apps/api/src/lib/guard.ts and
lib/tenancy.ts, and covered by apps/api/tests/tenancy.test.ts.
Sending OpenTelemetry traces
Any OTel SDK or Collector can export to Sentrinel:
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:3001
OTEL_EXPORTER_OTLP_PROTOCOL=http/json
OTEL_EXPORTER_OTLP_HEADERS=x-api-key=<your app key>
GenAI spans (gen_ai.request.model + gen_ai.usage.*_tokens) are additionally
priced and shown on the LLM cost page.
Cron monitoring
Create a monitor in the dashboard, then add its check-in URL to the job:
# at the end of the job
curl -fsS https://sentrinel.example.com/api/checkin/<token>
# or, for long jobs, report start / duration / explicit failure
curl -fsS "https://sentrinel.example.com/api/checkin/<token>?state=start"
curl -fsS "https://sentrinel.example.com/api/checkin/<token>?duration=1430"
curl -fsS "https://sentrinel.example.com/api/checkin/<token>?state=fail"
Deploy markers
Pass a version to the plugin and every release is recorded automatically:
sentrinelPlugin({ appName: "my-api", version: process.env.GIT_SHA })
IP โ location (optional)
| Variable | What it does |
|---|---|
SENTRINEL_GEOIP_DB |
Path to a GeoLite2-City.mmdb. Unset, request rows carry the country your CDN already reports and nothing else. |
Country has always come free โ Cloudflare, Vercel, Fastly and CloudFront all set a header and the plugin forwards it. City and coordinates need a real geo source, and this is the local one:
# Free, needs a MaxMind account:
# https://dev.maxmind.com/geoip/geolite2-free-geolocation-data
SENTRINEL_GEOIP_DB=/var/lib/sentrinel/GeoLite2-City.mmdb
Resolution happens in the API, never in an SDK โ the database is ~60MB and belongs in one place rather than embedded in every customer's application process. It is loaded once at boot and cached per address, so the ingest path pays a map read rather than a network call.
A lookup service was the alternative and was rejected deliberately: it would mean posting your customers' visitors' IP addresses to a third party on every request, in exchange for a city name.
Two things worth knowing:
- The edge header wins. A CDN resolves from the connecting address itself, which beats a database lookup on an address that has already been through a proxy. GeoLite2 fills in what the header cannot say.
- The file ages. Refresh it monthly, or addresses reassigned since will resolve to where they used to be.
Private and loopback addresses resolve to nothing rather than to a guess โ a
request from 10.x is somewhere, but nothing here can say where, and a fake
coordinate would put a dot on a map that means nothing.
CORS and the durable ingest log
# โโโ CORS โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# Any origin may call the API (SDKs, API keys, tokens). This list is only the
# origins allowed to send the *session cookie* โ the dashboard, and any other
# signed-in web app. Unset = localhost only, so a forgotten value breaks the
# dashboard loudly rather than leaking sessions quietly. "*" is an unsafe
# opt-out that hands every website a signed-in user's session; do not use it.
SENTRINEL_ALLOWED_ORIGINS=https://app.sentrinel.example.com
# โโโ Durable ingest log โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# Used automatically whenever a broker answers; the store is written directly
# when none does. Ingest fails over in both directions, so neither a dead
# broker nor a dead store loses telemetry. See PIPELINE.md.
SENTRINEL_REDPANDA_BROKERS=redpanda:9092 # unset = localhost:19092
SENTRINEL_INGEST_PIPELINE=auto # "redpanda" / "direct" pin the preference; neither disables failover
SENTRINEL_INGEST_FAILOVER_COOLDOWN_MS=30000 # how long a failed path is skipped
SENTRINEL_INGEST_PROBE_MS=15000 # how often a down path is re-tested
SENTRINEL_INGEST_PUBLISH_TIMEOUT_MS=5000 # caps what one slow publish costs ingest