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:

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