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 as SentinelDbContext or merged into your context with ApplySentinelModel(). Schema via EnsureCreated() 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.