Reference
Deployment
Embed Sentinel as packages, run the Sentinel Server container, or both — plus declarative YAML realm config with dry-run diffs, storage adapters, and the Relay bridge.
Sentinel’s topology bet is that you shouldn’t have to choose between a library and a server. The same public packages power both: embed them in your app, or run the prebuilt Sentinel Server container — a thin reference composition, not a fork. Anything the Server does, an embedding app can do.
Embedded
The default and the getting-started path: AddSentinel*
services, MapSentinel* endpoint groups, your process, your database, your deployment
pipeline. Identity lives at /auth/* of your own app; there is no second service to
operate, secure, or explain to the on-call rotation.
The Sentinel Server
Nuvora.Nexus.Sentinel.Server is a container-imaged ASP.NET Core host that mounts the
entire surface at default prefixes — auth, profile, passkeys, federation, OIDC,
workload federation, SAML, SCIM, admin API + console, webhooks, impersonation,
break-glass, privacy — plus /healthz (liveness) and /readyz (readiness: database,
signing-key ring, break-glass drill).
Configuration is env-driven (SENTINEL_*):
| Variable | Meaning | Default |
|---|---|---|
SENTINEL_ISSUER |
Token issuer | http://localhost:8080 |
SENTINEL_DB |
PostgreSQL connection string | absent → SQLite (sentinel.db) |
SENTINEL_VALKEY |
ValKey/Redis for hot state | absent → in-process |
SENTINEL_CONFIG |
Path to the declarative config file | — |
SENTINEL_OTLP |
OTLP exporter endpoint | — |
SENTINEL_ALLOW_DEV_KEYS |
Ephemeral signing keys (dev only) | false |
First boot provisions the bootstrap realm/org/admin and prints a one-time admin
password to stdout — no default credentials, ever. The repo ships
docker-compose.sentinel.yml (Sentinel + PostgreSQL 17 + ValKey 8) with
sentinel.config.yaml mounted at /etc/sentinel/.
Declarative configuration
Realms, orgs, roles, OIDC clients, IdP connections, SAML connections, workload trusts and webhooks can all be declared in versioned YAML/JSON and applied idempotently at boot (or programmatically):
version: 1
realms:
- key: default
isDefault: true
organizations:
- key: acme
displayName: Acme Health
domains:
- value: acme-health.example
kind: emailDomain
roles:
- key: org-admin
grants:
- pattern: "sentinel:org:manage"
effect: allow
oidcClients:
- clientId: acme-portal
clientType: confidential
secretRef: ACME_PORTAL_SECRET # the NAME of an env var — never the value
redirectUris: ["https://portal.acme-health.example/callback"]
The applier is diff-aware and honest: parsing rejects unknown properties, validation
fails before any write, dryRun: true produces a ConfigDiffReport
(creates / updates / unchanged / would-prune) without touching anything, and v1 never
deletes — prune is report-only. Secrets never live in the file; every secret-bearing
entry names an environment variable via secretRef, resolved through the
ISecretResolver port. The same definition-sync engine reconciles code-declared
permission catalogs at boot and fails the boot on unpublished permission ids.
Storage
Domain services depend on ports, never on a database client:
Stores.EfCore— the first-class persistence adapter: one provider-neutral relational model (PostgreSQL, SQL Server, SQLite), usable asSentinelDbContextor merged into your context withApplySentinelModel(). Schema viaEnsureCreated()this wave; shipped migrations land before 1.0.Stores.ValKey— the fleet hot-state adapter: rate counters and the typed pub/sub cache bus over ValKey/Redis. Without it, in-memory fallbacks keep a single-box deployment fully functional — no Redis required to start.- Snapshot caches are bounded (TTL + LRU); cache invalidation flows over typed cache-bus messages with an in-process loopback default.
The Relay bridge
Nuvora.Nexus.Sentinel.Relay connects Sentinel to a Relay
application without coupling either to the other:
builder.Services.AddSentinelRelayAuthorization();
app.UseRelayExceptionHandling();
app.UseAuthentication();
app.UseSentinelRelayAuthContext(); // replaces UseRelayAuthContext()
app.UseAuthorization();
The middleware projects the SentinelPrincipal and its permission snapshot onto Relay’s
AuthContext (Guid v7 subject → Relay’s Guid key), SentinelAuthorizationPolicy makes
[RequirePermission("service:scope:action")] evaluate against Sentinel’s engine, and
SentinelTenantResolver maps the org claim onto Relay’s TenantContext — an org-less
token resolves no tenant, so [TenantScoped] handlers fail closed. Sentinel itself
remains fully standalone; the bridge is one package deep.
Choosing
| Situation | Choice |
|---|---|
| One product, one team | Embed. Fewer moving parts wins. |
| Many apps, one identity domain | Sentinel Server as the shared IdP; apps are OIDC clients. |
| Platform team serving product teams | Server for the fleet; embedded for products that need in-process authorization checks. |
| Today a monolith, someday a fleet | Embed now — the Server speaks the same protocols and reads the same database when the day comes. |