# Beacon: integration guide > Everything needed to embed Beacon in an ASP.NET Core application, in the order you should do it. Start with the integration overview: it is a step-by-step checklist; the pages after it give every option for each step. Published at https://moberghr.github.io/beacon/. Every page is also available as Markdown at .md. --- # Integrate Beacon into your application > The step-by-step checklist for embedding Beacon in an ASP.NET Core host: packages, Program.cs, database, sign-in, jobs, connectors, AI, MCP and production, written for people and for coding agents. Source: https://moberghr.github.io/beacon/integrate/ Beacon ships as a set of NuGet packages (`Moberg.Beacon.*`). You add them to an ASP.NET Core application, wire them in `Program.cs`, and your application serves Beacon's UI at `/beacon`, its REST API at `/beacon/api`, and its MCP server at `/beacon/mcp`, under your own sign-in, your own job runner and your own deployment. This section is the complete integration guide. This page is the checklist; each step links to a page with every option for that step. :::tip[Using Claude Code or another coding agent?] Point it at the whole guide in one file: ```text Integrate Beacon into this ASP.NET Core app. Read https://moberghr.github.io/beacon/llms-integrate.txt first and follow its checklist step by step. Ask me before every decision the guide marks as one. ``` Every page of these docs is also available as Markdown: add `.md` to the page URL, or use **Copy as Markdown** under the page title. ::: ## Before you start | You need | Notes | |---|---| | .NET 10 SDK | Every Beacon package targets `net10.0`. | | An ASP.NET Core host | A minimal-API or MVC app. Beacon adds endpoints and middleware to it. | | A database for Beacon's own data | PostgreSQL with the `pgvector` extension available, or SQL Server. Beacon keeps everything in the `beacon` schema. See [Metadata database](https://moberghr.github.io/beacon/integrate/database/). | | A job runner | Beacon does not run background work itself. You implement `IBeaconScheduler` on your scheduler of choice; [Moberg Warp](https://moberghr.github.io/warp/) is the reference. See [Background jobs](https://moberghr.github.io/beacon/integrate/background-jobs/). | | An encryption key | 32 random bytes, base64-encoded, from a secret store. Beacon refuses to start without one. | | Node.js | Only if you build the UI package from source. Consumers of `Moberg.Beacon.UI` get the built SPA in the package. | ## Decisions to make first These choices shape the wiring. An agent should ask, not guess. | Decision | Options | Default if you have no preference | |---|---|---| | Metadata database | PostgreSQL (`Moberg.Beacon.Core.PostgreSql`) or SQL Server (`Moberg.Beacon.Core.SqlServer`) | PostgreSQL | | Who signs users in | Beacon's own user store and login form, your OIDC provider (Entra ID or any OIDC), or your app's existing sign-in | Beacon user store + OIDC if you already have an identity provider | | Job runner | Warp, Hangfire, Quartz.NET, or your own | Warp | | Which engines Beacon monitors | Any of nine connectors | Only the engines you have | | AI features | On (needs an LLM provider: OpenAI, Anthropic, Azure OpenAI or AWS Bedrock) or off | Off until you have a provider; MCP still needs the AI package | | MCP server | Exposed to AI clients or not | Exposed, with API keys | | Change approvals | Four-eyes review of query, migration, Custom SQL and recipient changes, off by default | On for regulated deployments | | Data migration (the only write path) | Off (default) or on | Off | ## The checklist 1. **Add the packages.** At minimum `Moberg.Beacon.Core`, a metadata-database provider, `Moberg.Beacon.Api`, `Moberg.Beacon.UI`, and one connector per engine you monitor. Add `Moberg.Beacon.AI` + `Moberg.Beacon.MCP` for the MCP server. → [Packages](https://moberghr.github.io/beacon/integrate/packages/) 2. **Set the two required secrets.** `ConnectionStrings:BeaconContext` and `Beacon:EncryptionKey` (`openssl rand -base64 32`). Never commit either. → [Host wiring](https://moberghr.github.io/beacon/integrate/host-setup/#configuration) 3. **Implement `IBeaconScheduler`** on your job runner, and make sure every job marks its DI scope as system work. Without a scheduler `AddBeaconServices` throws at startup. → [Background jobs](https://moberghr.github.io/beacon/integrate/background-jobs/) 4. **Register services in `Program.cs`:** `AddBeaconServices(...)` with your options, connectors, then `UsePostgreSql()` / `UseSqlServer()` last in the chain; cookie authentication with base path `/beacon`; Data Protection; `AddBeaconApiAuthorization()`, `AddBeaconApiServices()`, antiforgery; `AddBeaconAI` and `AddBeaconMcp` if you use them. → [Host wiring](https://moberghr.github.io/beacon/integrate/host-setup/) 5. **Build the middleware pipeline in the documented order.** The order is load-bearing: MCP challenge → API keys → JWT bearer → authentication → Beacon cookie → authorization → login form → first-run setup → antiforgery. → [Host wiring](https://moberghr.github.io/beacon/integrate/host-setup/#the-middleware-pipeline) 6. **Map the endpoints:** `MapBeaconApi()`, `MapLoginEndpoints("/beacon", …)`, `MapSetupEndpoints("/beacon")`, `MapBeaconMcp()`, `MapMcpDiscovery()`, `MapBeaconUi()`. → [Host wiring](https://moberghr.github.io/beacon/integrate/host-setup/#endpoints) 7. **Migrate Beacon's schema at startup** with `Beacon.Core.ServiceConfiguration.UseBeacon(app.Services, createSchema: true)`. Beacon does not migrate itself. → [Metadata database](https://moberghr.github.io/beacon/integrate/database/) 8. **Choose how people sign in:** Beacon's login form, OIDC/SSO (tenant allow-list, guest blocking, MFA), bearer JWTs, API keys. Plan the first-run admin and a break-glass account. → [Sign-in & identity](https://moberghr.github.io/beacon/integrate/authentication/) 9. **Decide who sees what:** projects are the access boundary; functional permissions split the Admin role; optional four-eyes approvals. → [Access control](https://moberghr.github.io/beacon/integrate/access-control/) 10. **Configure connectors** for the engines you monitor, each with a read-only database login. → [Data-source connectors](https://moberghr.github.io/beacon/integrate/connectors/) 11. **Optional, AI (required for MCP):** set `UseAI = true`, call `AddBeaconAI`, configure the LLM provider and, optionally, local embeddings. → [AI providers](https://moberghr.github.io/beacon/integrate/ai/) 12. **Optional, MCP (needs step 11):** expose `/beacon/mcp`, set `Beacon:PublicBaseUrl`, issue API keys or map Entra callers, connect Claude, Cursor or VS Code. → [MCP & AI clients](https://moberghr.github.io/beacon/integrate/mcp/) 13. **Optional, extend:** expose your own EF Core `DbContext`, your Markdown docs, or your own HTTP endpoints as MCP tools. → [Extending Beacon](https://moberghr.github.io/beacon/integrate/extending/) 14. **Harden for production:** Data Protection keys encrypted at rest, forwarded headers behind a proxy, deployment locks, audit retention, setup token, session affinity for MCP. → [Production checklist](https://moberghr.github.io/beacon/integrate/production/) 15. **Verify.** Start the app and check, in this order: - The console prints `Beacon first-run setup token (enter it on the setup page): …` (user management on, empty database). - `/beacon/setup` creates the first administrator, in the browser, or [headless](https://moberghr.github.io/beacon/integrate/authentication/#headless-first-run-scripts-and-ci). - `GET /beacon/api/health` answers `200` (until the first admin exists it redirects to `/beacon/setup`). - You can sign in and see **Home** at `/beacon/home`. - `GET /.well-known/mcp/server-card.json` lists the eight MCP tools (if you mapped MCP). - An API key with the Execute scope can list the MCP tools. See [Check it works](https://moberghr.github.io/beacon/integrate/mcp/#check-it-works). If something fails, see [Troubleshooting](https://moberghr.github.io/beacon/integrate/troubleshooting/). ## What you get ```mermaid flowchart LR subgraph Host["Your ASP.NET Core host"] direction TB UI["/beacon: React UI
Moberg.Beacon.UI"] API["/beacon/api: REST + SignalR hub
Moberg.Beacon.Api"] MCP["/beacon/mcp: MCP server
Moberg.Beacon.MCP"] Core["Beacon.Core: handlers, project access,
SQL execution gate, notifications"] Jobs["Your job runner
implements IBeaconScheduler"] UI --> API --> Core MCP --> Core Jobs --> Core end Core --> Meta[("Beacon metadata DB
schema beacon")] Core --> Sources[("Your databases
read-only logins")] Core --> Out["Email · Teams · Slack · Jira · Webhook"] Core --> LLM["LLM provider (optional)"] ``` Beacon never takes over the root of your site: everything lives under `/beacon` and `/.well-known/…` (MCP discovery). Your own routes, auth schemes and pages keep working. ## The reference host `src/Beacon.SampleProject/Program.cs` in the repository is a complete, running composition with every connector, Warp jobs, OIDC, bearer JWT for MCP, OpenTelemetry and forwarded headers. It is a sample, never shipped as a package. [Run it](https://moberghr.github.io/beacon/getting-started/quick-start/) to see a working Beacon before you embed one, and compare your `Program.cs` with it when something behaves differently. --- # Packages > Every Moberg.Beacon NuGet package, what it contains, which ones you need, and how versions are published. Source: https://moberghr.github.io/beacon/integrate/packages/ Beacon is published to nuget.org as `Moberg.Beacon.*` packages. Package IDs carry the `Moberg.` prefix; assemblies and namespaces stay `Beacon.*` (`using Beacon.Core;`). Every package targets `net10.0`. ## Which packages you need | Package | Contains | Needed when | |---|---|---| | `Moberg.Beacon.Core` | Domain, MediatR handlers, EF model, project access, SQL execution gate, notifications, scheduling contracts | Always (every other package depends on it) | | `Moberg.Beacon.Core.PostgreSql` | Beacon's metadata database on PostgreSQL: `UsePostgreSql(...)`, migrations, pgvector support | Beacon's own data lives in PostgreSQL | | `Moberg.Beacon.Core.SqlServer` | Beacon's metadata database on SQL Server: `UseSqlServer(...)`, migrations | Beacon's own data lives in SQL Server | | `Moberg.Beacon.Api` | REST endpoints under `/beacon/api`, the SignalR hub, cookie/OIDC/JWT/API-key authentication, login and first-run setup endpoints, antiforgery, Data Protection helpers | You serve the UI or the REST API (in practice always) | | `Moberg.Beacon.UI` | The built React app as a Razor Class Library, served at `/beacon` | You want the UI | | `Moberg.Beacon.AI` | LLM providers, the request queue, documentation generation, grounding, learning loop, AI actors | AI features, and always with MCP (MCP depends on it) | | `Moberg.Beacon.AI.Bedrock` | The Amazon Bedrock LLM provider (`AddBeaconBedrock()`) | You use Bedrock as the LLM provider | | `Moberg.Beacon.AI.Embeddings.Onnx` | Local in-process ONNX embeddings (`AddBeaconOnnxEmbeddings()`) | You want semantic retrieval; without it retrieval stays keyword-based | | `Moberg.Beacon.MCP` | The MCP server at `/beacon/mcp`, its tools, filters, discovery documents, host endpoint tools | You expose Beacon to AI clients | | `Moberg.Beacon.Connector.PostgreSql` | PostgreSQL connector | You monitor PostgreSQL | | `Moberg.Beacon.Connector.SqlServer` | SQL Server connector | You monitor SQL Server | | `Moberg.Beacon.Connector.MySql` | MySQL connector (also the MySQL migration-write path) | You monitor MySQL | | `Moberg.Beacon.Connector.AzureSynapse` | Azure Synapse connector | You monitor Synapse | | `Moberg.Beacon.Connector.Snowflake` | Snowflake connector | You monitor Snowflake | | `Moberg.Beacon.Connector.BigQuery` | Google BigQuery connector | You monitor BigQuery | | `Moberg.Beacon.Connector.Databricks` | Databricks SQL connector | You monitor Databricks | | `Moberg.Beacon.Connector.CloudWatch` | AWS CloudWatch Logs Insights connector | You monitor CloudWatch | | `Moberg.Beacon.Connector.Api` | REST API connector (OpenAPI import, JSON to rows) | You monitor HTTP APIs | :::caution[Two different "SQL Server" packages] `Moberg.Beacon.Core.SqlServer` stores **Beacon's own data** in SQL Server (`UseSqlServer`). `Moberg.Beacon.Connector.SqlServer` lets Beacon **monitor** a SQL Server database (`AddSqlServerConnector`). If both are true, add both. The same split applies to PostgreSQL. ::: ## Typical sets **PostgreSQL metadata, monitoring PostgreSQL, with UI and MCP:** ```bash dotnet add package Moberg.Beacon.Core dotnet add package Moberg.Beacon.Core.PostgreSql dotnet add package Moberg.Beacon.Api dotnet add package Moberg.Beacon.UI dotnet add package Moberg.Beacon.AI dotnet add package Moberg.Beacon.MCP dotnet add package Moberg.Beacon.Connector.PostgreSql ``` **SQL Server shop, no AI, no MCP:** ```bash dotnet add package Moberg.Beacon.Core dotnet add package Moberg.Beacon.Core.SqlServer dotnet add package Moberg.Beacon.Api dotnet add package Moberg.Beacon.UI dotnet add package Moberg.Beacon.Connector.SqlServer ``` Add your job runner too. For the reference implementation, use the `Moberg.Warp.*` packages (see [Background jobs](https://moberghr.github.io/beacon/integrate/background-jobs/)). ## Dependency direction ```text Moberg.Beacon.Api → Core Moberg.Beacon.AI → Core Moberg.Beacon.MCP → Core, AI Moberg.Beacon.Core.* → Core Moberg.Beacon.Connector.* → Core Moberg.Beacon.UI → (nothing; static web assets) ``` `Moberg.Beacon.Core` itself references no server database driver or EF Core provider (it carries `Microsoft.Data.Sqlite` for the in-memory joins of cross-source queries). The SQL Server EF provider comes only with `Moberg.Beacon.Core.SqlServer`; Npgsql and pgvector only with `Moberg.Beacon.Core.PostgreSql` or the PostgreSQL connector; each connector brings its own driver. ## Versions - There is no version in the source code. The git release tag is the package version: publishing a GitHub release packs every packable project with that tag and pushes it to nuget.org. Four-part tags are normalised by NuGet (a tag `4.5.0.0` publishes `4.5.0`). - Keep every `Moberg.Beacon.*` package on the **same version**. They are built and released together. - These docs track the `main` branch. Check the [release notes](https://github.com/moberghr/beacon/releases) of the version you install for what it includes. If you use central package management, pin one version for all of them: ```xml ``` ## Build-time notes - **`Microsoft.AspNetCore.OpenApi`.** If your host calls `AddOpenApi()`/`MapOpenApi()`, reference `Microsoft.AspNetCore.OpenApi` directly; relying on the copy that comes with `Moberg.Beacon.Api` fails the build with `CS9137` (interceptors). - **Transitive package advisories.** If restore reports `NU1902`/`NU1903` for a transitive dependency and you treat warnings as errors, add a direct `PackageReference` to the patched version of that package in your project. - **The UI package** contains the production React build. You do not need Node.js to consume it. The host must call `app.UseStaticFiles()` so the assets are served. - **Building from source** (project references instead of packages): `Beacon.UI` builds the React app only in `Release`, or in `Debug` with `-p:BuildReactInDebug=true`. Node.js 22 and npm 11 match the CI build. ## Licence Beacon is dual-licensed: GNU AGPL v3.0, or a commercial licence without copyleft obligations. Moberg partners use it under a licence exception. Embedding Beacon in a network service under the AGPL triggers its source-disclosure terms (§13). See [LICENSING.md](https://github.com/moberghr/beacon/blob/main/LICENSING.md). --- # Host wiring > A complete, annotated Program.cs for embedding Beacon: service registration, the load-bearing middleware order, endpoint mapping, startup migration, and every option of AddBeaconServices. Source: https://moberghr.github.io/beacon/integrate/host-setup/ This page gives you a complete `Program.cs` and explains each part. Copy it, then remove what you do not use. The reference host in the repository, `src/Beacon.SampleProject/Program.cs`, follows the same shape. ## Configuration Two values are required before anything starts. Keep both out of source control: user-secrets in development, a secret store or environment variables everywhere else. | Key | Environment variable | Value | |---|---|---| | `ConnectionStrings:BeaconContext` | `ConnectionStrings__BeaconContext` | Connection string of Beacon's metadata database | | `Beacon:EncryptionKey` | `Beacon__EncryptionKey` | Base64 of at least 32 random bytes: `openssl rand -base64 32` | ```bash dotnet user-secrets init dotnet user-secrets set "ConnectionStrings:BeaconContext" "Host=localhost;Database=beacon;Username=beacon;Password=…" dotnet user-secrets set "Beacon:EncryptionKey" "$(openssl rand -base64 32)" ``` The database must exist; Beacon creates its schema and tables in it (see [Metadata database](https://moberghr.github.io/beacon/integrate/database/)). Beacon checks the encryption key in every environment and refuses to start when it is missing, shorter than 32 bytes, not random-looking (all printable ASCII, or fewer than 16 distinct byte values), a published example value, or an unexpanded `${…}` placeholder. Outside `Development` you also need `Beacon:DataProtection:ApplicationName` (see [Data Protection](#data-protection)). The [configuration reference](https://moberghr.github.io/beacon/getting-started/configuration/) lists every other key. ## The complete Program.cs This host keeps Beacon's metadata in PostgreSQL, monitors PostgreSQL, signs users in with Beacon's own user store (plus optional OIDC), runs jobs on Warp, and exposes the MCP server. `BeaconScheduler` and `BeaconClaimsTransformation` are small classes you add to your project. See [Background jobs](https://moberghr.github.io/beacon/integrate/background-jobs/) and [the claims transformation](#the-claims-transformation) below. ```csharp using Beacon.AI; // AddBeaconAI using Beacon.Api; // AddBeaconApiServices using Beacon.Api.Authentication; // cookie/OIDC/JWT/Data Protection helpers + middleware using Beacon.Api.Endpoints; // AddBeaconApiAuthorization, MapBeaconApi, MapLoginEndpoints, MapSetupEndpoints using Beacon.Connector.PostgreSql; // AddPostgreSqlConnector using Beacon.Core; // AddBeaconServices, BeaconConfiguration using Beacon.Core.Authentication.Providers; // DatabaseAuthenticationProvider using Beacon.Core.PostgreSql; // UsePostgreSql using Beacon.MCP; // AddBeaconMcp, MapBeaconMcp using Beacon.MCP.Discovery; // MapMcpDiscovery, UseMcpResourceMetadataChallenge using Beacon.UI; // MapBeaconUi, BeaconUiEndpointRouteBuilderExtensions.BasePath var builder = WebApplication.CreateBuilder(args); const string beaconBasePath = BeaconUiEndpointRouteBuilderExtensions.BasePath; // "/beacon" // AI work (documentation, ask) can run for minutes; keep server and HttpClient timeouts long. builder.WebHost.ConfigureKestrel(x => { x.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(5); x.Limits.RequestHeadersTimeout = TimeSpan.FromMinutes(5); }); builder.Services.AddHttpClient().ConfigureHttpClientDefaults(x => x.ConfigureHttpClient(y => y.Timeout = TimeSpan.FromMinutes(5))); // Your job runner (here: Warp). See "Background jobs". builder.Services.AddBeaconWarp(builder.Configuration); // Claims transformation + SignalR user id provider. builder.Services.AddBeaconHostInfrastructure(); // 1. Beacon core, connectors, metadata database (UsePostgreSql ends the chain). builder.Services.AddBeaconServices(builder.Configuration, options => { options.AddBeaconScheduler(); // required options.BaseUrl = "https://beacon.example.com/beacon"; // deep links in notifications; include /beacon options.UseAI = true; // needs AddBeaconAI below options.Authorization.Enabled = true; options.Authentication.EnableLoginForm = true; options.AddAuthenticationProvider(); options.EnableUserManagement(); // options.EnableApprovalWorkflow(x => x.RequireApprovedVersionToRun = true); // four-eyes, opt-in // options.AddEmailAdapter(); // your IEmailAdapter }) .AddPostgreSqlConnector() .UsePostgreSql(); // ConnectionStrings:BeaconContext // 2. Sign-in. builder.Services.AddBeaconCookieAuthentication(beaconBasePath); builder.Services.AddBeaconDataProtection(builder.Configuration, builder.Environment); // Outside Development chain .ProtectKeysWithCertificate(...) or .ProtectKeysWithAzureKeyVault(...) here. builder.Services.AddBeaconOidcAuthentication(builder.Configuration); // no-op unless Beacon:Authentication:Oidc:Enabled // 3. AI and MCP (optional). builder.Services.AddBeaconAI(builder.Configuration); builder.Services.AddBeaconMcp(); // 4. REST API, policies, realtime, antiforgery, OpenAPI. builder.Services.AddAntiforgery(x => { x.HeaderName = "X-XSRF-TOKEN"; x.Cookie.Name = ".Beacon.Antiforgery"; }); builder.Services.AddBeaconApiAuthorization(); // BeaconApi, Admin, Execute-scope and permission policies builder.Services.AddBeaconApiServices(); // SignalR hub + notifiers; x => x.Realtime = false to turn off builder.Services.AddOpenApi(); var app = builder.Build(); var beaconConfiguration = app.Services.GetRequiredService(); // Beacon does not migrate itself: create the schema and apply migrations at startup. Beacon.Core.ServiceConfiguration.UseBeacon(app.Services, createSchema: true); // ── Middleware: the order below is load-bearing ────────────────────────── app.UseHttpsRedirection(); // see "HTTPS redirection" below for local MCP over HTTP app.UseStaticFiles(); // serves the UI bundle app.UseMcpResourceMetadataChallenge(); // adds resource_metadata to 401s from /beacon/mcp app.UseBeaconAuthentication(); // API key, bearer JWT, UseAuthentication, Beacon.Auth cookie app.UseAuthorization(); app.UseMiddleware(beaconConfiguration, beaconBasePath); app.UseMiddleware(beaconConfiguration, beaconBasePath); app.UseAntiforgery(); // ── Endpoints ──────────────────────────────────────────────────────────── app.MapOpenApi(); // /openapi/v1.json app.MapBeaconApi(); // /beacon/api/* and the /beacon/api/hub SignalR hub app.MapLoginEndpoints(beaconBasePath, beaconConfiguration); // /beacon/api/auth/login, logout, sso/challenge app.MapSetupEndpoints(beaconBasePath); // /beacon/api/setup/* (first-run admin) app.MapBeaconMcp(); // /beacon/mcp, Execute scope required app.MapMcpDiscovery(); // /.well-known/oauth-protected-resource, server card app.MapBeaconUi(); // React app at /beacon await app.StartBeaconWarpAsync(); // your job runner's startup (see "Background jobs") app.Run(); ``` `AddBeaconWarp` and `StartBeaconWarpAsync` are two small extension methods you write once; [Background jobs](https://moberghr.github.io/beacon/integrate/background-jobs/) shows them. With another job runner, replace both lines. `AddOpenApi()` and `MapOpenApi()` need a direct reference to `Microsoft.AspNetCore.OpenApi` in your project (`dotnet add package Microsoft.AspNetCore.OpenApi`); getting it only through `Moberg.Beacon.Api` fails the build with `CS9137`. Drop both lines if you do not want an OpenAPI document. ## What each part does ### `AddBeaconServices` ```csharp public static BeaconBuilder AddBeaconServices( this IServiceCollection services, IConfiguration configuration, Action beaconConfiguration) ``` It registers Beacon's core services and returns a `BeaconBuilder`. Chain connectors and host extensions on the builder, and finish with `UsePostgreSql(...)` or `UseSqlServer(...)`. Those return `IServiceCollection`, so they end the chain. ```csharp builder.Services.AddBeaconServices(builder.Configuration, options => { /* … */ }) .AddPostgreSqlConnector() // connectors: BeaconBuilder → BeaconBuilder .ExposeDbContext(…) // host extensions: BeaconBuilder → BeaconBuilder .UsePostgreSql(); // metadata DB: BeaconBuilder → IServiceCollection (last) ``` `BeaconConfiguration` is set **in code** through the lambda; it is not bound from `appsettings.json`. | Option | Default | What it does | |---|---|---| | `AddBeaconScheduler()` | None (required) | Your `IBeaconScheduler`. Without one, `AddBeaconServices` throws `Implementation of IBeaconScheduler is required.` | | `BaseUrl` | `null` | Public URL of the UI, **including `/beacon`** (e.g. `https://beacon.example.com/beacon`). Notification messages link to `{BaseUrl}/notifications/{id}`. Editable later in **Admin settings**. | | `UseAI` | `false` | Turns AI features on. Also call `AddBeaconAI`. | | `Authorization.Enabled` | `false` | Role-based authorization. Turn it on in every real deployment. | | `AddAuthorizationProvider()` | user store when user management is on, otherwise allow-all | Custom `IBeaconAuthorizationProvider` (global read/write ceiling per user). | | `Authentication.EnableLoginForm` | `false` | Beacon's login page and the redirect of signed-out browsers to `/beacon/login`. | | `AddAuthenticationProvider()` | user store when user management is on | Password check. `DatabaseAuthenticationProvider` uses Beacon's user store. | | `Authentication.CookieExpirationHours` / `RememberMeExpirationDays` | `24` / `30` | Requested cookie lifetime; always capped by the session's absolute lifetime (12 h by default). | | `Authentication.LoginRedirectPath` | `/beacon/home` | Where the login page sends a user after sign-in. | | `EnableUserManagement(x => …)` | off | Beacon's user store: users, roles, first-run setup, password policy (`MinimumPasswordLength` 8, `RequirePasswordComplexity` true, `AllowInternalUsers` true). | | `EnableApprovalWorkflow(x => …)` | off | Four-eyes review of query changes and, by default, migration jobs, Custom SQL rules and recipient destinations. See [Approvals](https://moberghr.github.io/beacon/features/approvals/). | | `AddEmailAdapter()` | none | Your `IEmailAdapter`. Without one there is no email channel. | | `SavedQueryTools.NamedToolLimit` | `25` | Above this many published saved queries, MCP lists two generic tools instead of one tool per query. | Only two parts of user management are also read from configuration: `Beacon:UserManagement:SetupToken` and `Beacon:UserManagement:LocalLogin:*`. ### The metadata database `UsePostgreSql()` and `UseSqlServer()` read `ConnectionStrings:BeaconContext`. Overloads take a connection string, or, on PostgreSQL, your own `NpgsqlDataSource`. The schema is always `beacon`. Details: [Metadata database](https://moberghr.github.io/beacon/integrate/database/). ### Authentication helpers | Call | What it does | |---|---| | `AddBeaconHostInfrastructure()` | Registers your claims transformation and the SignalR user-id provider. | | `AddBeaconCookieAuthentication(basePath)` | The `Beacon.Auth` cookie (HttpOnly, SameSite=Lax, Secure on HTTPS), server-side session revalidation, JSON 401/403 for API and MCP calls instead of redirects. **Pass `"/beacon"`**, so the login path is `/beacon/login`. | | `AddBeaconDataProtection(configuration, environment)` | Stores the Data Protection key ring in Beacon's database. Skip it if your host already configures Data Protection. | | `AddBeaconOidcAuthentication(configuration)` | OIDC sign-in from `Beacon:Authentication:Oidc` (does nothing unless `Enabled`). | | `AddBeaconJwtAuthentication(jwt => …)` | Bearer JWTs for the REST API and MCP (code-configured). | All options are on [Sign-in & identity](https://moberghr.github.io/beacon/integrate/authentication/). ### API services | Call | What it does | |---|---| | `AddBeaconApiAuthorization()` | Registers the `BeaconApi`, `BeaconApiAdmin` and Execute-scope policies plus one policy per functional permission. Required by `MapBeaconApi` and `MapBeaconMcp`. | | `AddBeaconApiServices()` | SignalR, the login rate limiter, notifiers, and the request principal Beacon's core reads the caller from (`IBeaconRequestPrincipal`; `AddBeaconCookieAuthentication` and `AddBeaconMcp` register it too, and startup fails when Beacon's endpoints are mapped without it). Options: `Realtime` (default `true`), `ConfigureSignalR` (e.g. a Redis backplane), `ExternalLogin` (when your app owns sign-in). | | `AddAntiforgery(...)` | The UI sends the antiforgery token in `X-XSRF-TOKEN`; it gets it from `GET /beacon/api/csrf`. API-key and bearer callers are exempt. | :::caution[Upgrading to 5.0: the request principal] In 5.0 an `IHttpContextAccessor` alone no longer tells Beacon's core who the request's caller is. A web host calls `AddBeaconApiServices`, `AddBeaconCookieAuthentication` or `AddBeaconMcp` (each registers the request principal), or registers its own with `UseRequestPrincipal()`. A host that registers only Beacon's core and an accessor serves every request as if no request were in progress. ::: ```csharp // Realtime off: no SignalR, no hub route; the UI simply does not connect. builder.Services.AddBeaconApiServices(x => x.Realtime = false); // Several instances: a SignalR backplane. builder.Services.AddBeaconApiServices(x => x.ConfigureSignalR = signalR => signalR.AddStackExchangeRedis(redisConnectionString)); // Your app owns sign-in: the UI sends signed-out users to your login page. builder.Services.AddBeaconApiServices(x => x.ExternalLogin = new ExternalLoginOptions { LoginUrl = "/account/login", ReturnUrlParameter = "returnUrl" }); ``` ## The middleware pipeline The order is load-bearing. Each step depends on the ones before it: | # | Middleware | Why it sits here | |---|---|---| | 1 | `UseForwardedHeaders` (behind a proxy) | Everything after it must see the client's address and scheme: the login throttle is per IP, the cookie's Secure flag follows the scheme. | | 2 | `UseHttpsRedirection` | API keys travel in headers; never accept them over plain HTTP outside development. | | 3 | `UseStaticFiles` | Serves the UI bundle before authentication runs. | | 4 | Your exception handler (optional) | Turns Beacon's exceptions into problem+json. See [below](#error-responses). | | 5 | `UseMcpResourceMetadataChallenge` | Decorates every 401 from `/beacon/mcp` with the RFC 9728 `resource_metadata` pointer; must precede the middleware that writes those 401s. | | 6 | `ApiKeyAuthMiddleware` (`UseBeaconAuthentication` wires steps 6 to 9) | Authenticates `Authorization: Bearer sk-sem_…` before cookie auth could redirect an MCP client to a login page. | | 7 | `UseBeaconJwtBearerAuthentication` | Bearer JWTs (Entra and other OIDC access tokens). | | 8 | `UseAuthentication` | Your own schemes. | | 9 | `BeaconCookieAuthMiddleware` | Fills `HttpContext.User` from the `Beacon.Auth` cookie **before** authorization evaluates policies. | | 10 | `UseAuthorization` | Endpoint policies. | | 11 | `LoginFormAuthMiddleware` | Redirects signed-out browsers to `/beacon/login` (only when `EnableLoginForm`). | | 12 | `FirstRunSetupMiddleware` | Until the first admin exists, redirects Beacon's requests (`/beacon/api/health` included) to `/beacon/setup` (only with user management). | | 13 | `UseAntiforgery` | After authentication, so tokens are issued for the signed-in user. | Steps 6, 7, 9, 11 and 12 act on `/beacon` and the prefixes in `Beacon:Authentication:AdditionalPaths` only: a Beacon API key, bearer token or `Beacon.Auth` cookie never authenticates another route of your application, and your routes are never redirected to Beacon's login or setup page. Beacon's cookie scheme refuses the session on those other routes as well, so a host whose default scheme is the cookie still never has its own routes authenticated by it. List `/warp` there when you serve the Warp dashboard with Beacon's admin check: a host upgraded from a version without path scoping must add it, or the dashboard refuses every request. See [Where Beacon credentials apply](https://moberghr.github.io/beacon/getting-started/configuration/#where-beacon-credentials-apply). :::danger[Do not reorder or assign `context.User` later] `ApiKeyAuthMiddleware` and `BeaconCookieAuthMiddleware` record their result as the request's authentication result; SignalR uses it to close connections when a session ends. Reordering them, or assigning `context.User` after them, breaks API-key callers, the login redirect and session revocation. ::: ## Endpoints | Call | Maps | |---|---| | `MapBeaconApi()` | `/beacon/api/*` (cookie or API key, `BeaconApi` policy, antiforgery and permission filters) and the SignalR hub `/beacon/api/hub`. Migration endpoints only when `Beacon:Migrations:Enabled`. | | `MapLoginEndpoints("/beacon", configuration)` | `POST /beacon/api/auth/login` (only when password sign-in is offered), `POST /beacon/api/auth/logout`, `GET /beacon/api/auth/sso/challenge`. | | `MapSetupEndpoints("/beacon")` | `GET /beacon/api/setup/status`, `POST /beacon/api/setup/superadmin`, `GET /beacon/api/setup/roles`. Extension on `WebApplication`. Map it only with user management on. | | `MapBeaconMcp()` | `/beacon/mcp` with the Execute-scope policy. Never map MCP yourself with `MapMcp(...)` and a bare `RequireAuthorization()`. | | `MapMcpDiscovery()` | Anonymous `/.well-known/oauth-protected-resource`, `/.well-known/oauth-protected-resource/beacon/mcp`, `/.well-known/mcp/server-card.json`. | | `MapBeaconUi()` | The React app at `/beacon` and its client routes. Your app keeps `/`. | | `MapOpenApi()` | Your OpenAPI document, which includes Beacon's endpoints. Optional. | The full route list is on [Routes & endpoints](https://moberghr.github.io/beacon/reference/routes/). ## The claims transformation `AddBeaconHostInfrastructure()` needs an `IClaimsTransformation`. With Beacon's user store, this one adds the user id and name claims and resolves a global role from the store by the session's external id, never from a name the user typed. It behaves like the reference host's `SampleClaimsTransformation`; Beacon's own role constants are internal, so it carries the role names and levels itself: ```csharp using System.Security.Claims; using Beacon.Core.Authorization; using Beacon.Core.Models.UserManagement; using Beacon.Core.Services; using Microsoft.AspNetCore.Authentication; public sealed class BeaconClaimsTransformation(IUserManagementService users) : IClaimsTransformation { public async Task TransformAsync(ClaimsPrincipal principal) { if (principal.Identity is not ClaimsIdentity identity || !identity.IsAuthenticated) { return principal; } var name = identity.Name ?? "unknown"; if (!identity.HasClaim(x => x.Type == BeaconClaims.UserId)) { identity.AddClaim(new Claim(BeaconClaims.UserId, name)); } if (!identity.HasClaim(x => x.Type == BeaconClaims.UserName)) { identity.AddClaim(new Claim(BeaconClaims.UserName, name)); } var externalId = identity.FindFirst(ClaimTypes.NameIdentifier)?.Value; if (identity.HasClaim(x => x.Type == BeaconClaims.Role) || string.IsNullOrWhiteSpace(externalId)) { return principal; } var user = await users.GetUserByExternalIdAsync(externalId); var role = user is { IsEnabled: true } ? ResolveRole(user) : null; if (role != null) { identity.AddClaim(new Claim(BeaconClaims.Role, role)); } return principal; } private static string? ResolveRole(BeaconUserData user) { if (user.IsSuperAdmin) { return "Admin"; } if (user.Roles.Count == 0) { return null; } // Role levels: Admin 3, Editor 2, Viewer 1. Functional roles (level 0) carry no global role. var level = user.Roles.Max(x => x.Level); return level switch { >= 3 => "Admin", 2 => "Editor", 1 => "Viewer", _ => null, }; } } ``` ## Error responses Beacon's handlers throw `InvalidOperationException` for business-rule violations, `BeaconException` for domain errors and `SettingLockedException` when a deployment lock refuses a change; access refusals are already mapped to 403/404 by `MapBeaconApi`. Map the rest to problem+json for `/beacon/api` so the UI shows the message instead of a generic 500: ```csharp using Beacon.Core.Models; app.UseWhen(x => x.Request.Path.StartsWithSegments("/beacon/api"), branch => branch.Use(async (context, next) => { try { await next(context); } catch (Exception ex) when (!context.Response.HasStarted && !context.RequestAborted.IsCancellationRequested) { var (status, title, type) = ex switch { UnauthorizedAccessException or BeaconForbiddenException => (403, "Forbidden", "/errors/forbidden"), SettingLockedException x => (409, x.Message, "/errors/setting-locked"), InvalidOperationException x => (400, x.Message, "/errors/invalid-operation"), BeaconException x => (400, x.Message, "/errors/beacon"), _ => (500, "Internal server error", "/errors/internal"), }; context.Response.StatusCode = status; await context.Response.WriteAsJsonAsync(new { type, title, status }, (System.Text.Json.JsonSerializerOptions?)null, "application/problem+json"); } })); ``` Place it after `UseStaticFiles` and before the authentication middleware, so their exceptions are mapped too. ## Data Protection The `Beacon.Auth` cookie and antiforgery tokens are protected with ASP.NET Core Data Protection, which belongs to your host: - **Your host already configures Data Protection** (shared key ring, Azure Blob, Redis): skip `AddBeaconDataProtection`. - **Otherwise** call `AddBeaconDataProtection(configuration, environment)`. It keeps the key ring in Beacon's database (`data_protection_keys`) under the application name `Beacon:DataProtection:ApplicationName`, which is required outside `Development` and takes one name per environment. Outside `Development` the key ring must be encrypted at rest or the host does not start. Chain a protector: ```csharp builder.Services.AddBeaconDataProtection(builder.Configuration, builder.Environment) .ProtectKeysWithCertificate(certificate); // or .ProtectKeysWithAzureKeyVault(keyId, credential) ``` `Beacon:DataProtection:AllowUnencryptedKeys=true` acknowledges an unencrypted ring instead (logged as a warning). With encryption on, keys that were stored unencrypted are revoked at startup, so everyone signs in once more. ## HTTPS redirection `UseHttpsRedirection` belongs in every deployment: API keys travel in headers. On a development machine running plain HTTP, local MCP clients connect over HTTP, so the reference host skips the redirect for `/beacon/mcp` in Development only: ```csharp if (app.Environment.IsDevelopment()) { app.UseWhen(x => !x.Request.Path.StartsWithSegments("/beacon/mcp"), y => y.UseHttpsRedirection()); } else { app.UseHttpsRedirection(); } ``` ## Minimal variants **No MCP, no AI.** Drop the `Moberg.Beacon.AI` and `Moberg.Beacon.MCP` packages, `UseAI`, `AddBeaconAI`, `AddBeaconMcp`, `UseMcpResourceMetadataChallenge`, `MapBeaconMcp` and `MapMcpDiscovery`. **Your app owns sign-in.** Leave user management and the login form off, set `BeaconApiOptions.ExternalLogin`, and make sure your authentication produces a principal with `ClaimTypes.NameIdentifier` and a Beacon role claim (`Admin`, `Editor` or `Viewer`). Without Beacon's user store, only the `Admin` role claim grants functional permissions unless `Beacon:Authorization:Roles:HonourHostRoleClaims` is `true`. See [Sign-in & identity](https://moberghr.github.io/beacon/integrate/authentication/#your-app-owns-sign-in). **SQL Server metadata.** Replace `Moberg.Beacon.Core.PostgreSql` with `Moberg.Beacon.Core.SqlServer` and `UsePostgreSql()` with `UseSqlServer()`. Warp needs PostgreSQL for its own tables; with SQL Server, use another runner or give Warp a PostgreSQL database. --- # Metadata database > Where Beacon keeps its own data: PostgreSQL with pgvector or SQL Server, the fixed beacon schema, how to create and migrate it, and how to hand Beacon your own NpgsqlDataSource. Source: https://moberghr.github.io/beacon/integrate/database/ Beacon stores its own data (users, projects, data sources, queries, subscriptions, runs, audit rows, grounding) in a **metadata database**. This is separate from the databases Beacon monitors, although it can live on the same server. ## Choose a provider | | PostgreSQL | SQL Server | |---|---|---| | Package | `Moberg.Beacon.Core.PostgreSql` | `Moberg.Beacon.Core.SqlServer` | | Registration | `.UsePostgreSql(...)` | `.UseSqlServer(...)` | | Naming | snake_case tables and columns | Pascal case | | Semantic retrieval | `pgvector` column + HNSW index | Vectors compared in memory | | Extra requirement | The `vector` extension must be available on the server | None | Both providers carry their own EF Core migrations and are kept in step: every schema change ships for both. ## The `beacon` schema Everything lives in the schema `beacon`, including the migration history table (`beacon.__EFMigrationsHistory`). The schema is **not configurable**: migration snapshots name it, so `Beacon:Schema` set to anything other than `beacon` makes startup fail with an explanation. Leave the key unset. ## Registration ```csharp // Connection string from ConnectionStrings:BeaconContext .UsePostgreSql(); // Explicit connection string .UsePostgreSql(connectionString); // Your own NpgsqlDataSource: Aspire, Entra passwordless auth, a shared pool .UsePostgreSql(sp => sp.GetRequiredService()); .UsePostgreSql(dataSource); // SQL Server .UseSqlServer(); .UseSqlServer(connectionString); ``` Every overload also takes a `schema` argument; it only accepts `"beacon"`. `UsePostgreSql` / `UseSqlServer` register an `IDbContextFactory` and a scoped `BeaconContext` (Data Protection persists its key ring through it). Call them **last** in the `AddBeaconServices(...)` chain: they return `IServiceCollection`, and connectors, `ExposeDbContext` and `ExposeDocs` must come before. ### Bringing your own `NpgsqlDataSource` When you pass a data source (or a factory resolved once from the root provider), Beacon: - runs `SET search_path TO "beacon", public` on every connection it opens, so its unqualified SQL and the `vector` type resolve; - does not need `UseVector()` on your data source, because it never binds the vector type; - expects connection reset to stay on (no `No Reset On Close=true`) and `Multiplexing` off. With a connection string, Beacon builds its own data source with `SearchPath=beacon,public`. ## Creating and migrating the schema **Beacon does not migrate its schema by itself.** Create the **database** first (`CREATE DATABASE beacon;`, or `POSTGRES_DB=beacon` for the container image). Beacon creates the schema and tables, not the database. Then call `UseBeacon` once at startup, after `builder.Build()`: ```csharp var app = builder.Build(); // Creates the beacon schema if missing, then applies pending EF Core migrations. Beacon.Core.ServiceConfiguration.UseBeacon(app.Services, createSchema: true); ``` `UseBeacon` is a plain static method on `Beacon.Core.ServiceConfiguration` (several Beacon packages have a class called `ServiceConfiguration`; use the full name). It is synchronous. | Situation | Approach | |---|---| | One instance, or a dev machine | `UseBeacon(app.Services, createSchema: true)` at startup. | | Several instances starting together | Run migrations once, from a deployment step, a dedicated migrator process that calls `UseBeacon`, or an idempotent SQL script, rather than from every replica. | | The application login cannot create schemas or extensions | Pre-create the `beacon` schema (and on PostgreSQL the `vector` extension) as an administrator, grant the application login ownership of the schema, then call `UseBeacon(app.Services)` without `createSchema`. | ### pgvector on PostgreSQL One migration runs `CREATE EXTENSION IF NOT EXISTS vector;` and adds a `vector(384)` column with an HNSW index. The extension must be **available** on the server (`SELECT * FROM pg_available_extensions WHERE name = 'vector';` returns a row) and the migrating login must be allowed to create it, or an administrator must install it first. - Azure Database for PostgreSQL – Flexible Server: add `VECTOR` to the `azure.extensions` server parameter. - Amazon RDS / Aurora PostgreSQL: pgvector is available on current engine versions. - Self-hosted: install the pgvector package for your PostgreSQL major version, then restart. If the extension is installed in `public`, Beacon's search path (`beacon, public`) finds it. ### Generating SQL instead of migrating at runtime Migrations live in the provider assemblies. To produce a script from a checkout of the Beacon repository: ```bash dotnet tool install --global dotnet-ef dotnet ef migrations script --idempotent \ --project src/Beacon.Core.PostgreSql \ --startup-project src/Beacon.SampleProject \ --context PostgreSqlBeaconContext \ --output beacon-postgresql.sql ``` The startup project needs `ConnectionStrings:BeaconContext` configured. `--context` is required because the sample host has more than one `DbContext`. The migrations create the `beacon` schema and the `vector` extension themselves. ## Sizing and housekeeping - Run history, MCP audit rows and learning signals grow with use. Set `Beacon:Mcp:Audit:RetentionDays` and schedule the audit purge (see [Background jobs](https://moberghr.github.io/beacon/integrate/background-jobs/#recurring-maintenance-jobs)); learning signals are cleaned by the `CleanupOldSignals` job after the project's retention period (90 days by default). - Back up the metadata database like any application database. It holds encrypted connection strings and tokens; they are useless without `Beacon:EncryptionKey`, so keep the key's backup separate. - Beacon's own data access uses short-lived contexts from `IDbContextFactory`; size the pool for your job runner's worker count plus web traffic. The reference host caps Warp at `min(2 × CPU count, 20)` workers for this reason. --- # Sign-in & identity > Every way a person or program proves who it is to Beacon: first-run setup, local passwords and break-glass accounts, OIDC/Entra SSO with MFA, bearer JWTs, API keys, sessions and your own sign-in. Source: https://moberghr.github.io/beacon/integrate/authentication/ Beacon recognises five kinds of caller. You can combine them. | Caller | How it authenticates | Typical use | |---|---|---| | Person, password | Beacon's login form against Beacon's user store | Small teams, break-glass | | Person, SSO | OIDC code flow (Entra ID or any OIDC provider) | Organisations with an identity provider | | Person, your app | Your host's own authentication scheme | Beacon embedded inside your product | | Program, API key | `Authorization: Bearer sk-sem_…` | Scripts, CI, MCP clients | | Program or delegated user, JWT | `Authorization: Bearer ` | Entra-authenticated MCP clients, service-to-service | ## Beacon's user store and first-run setup `options.EnableUserManagement()` keeps users and roles in Beacon's database. While no administrator exists: 1. Every browser request is redirected to `/beacon/setup`. 2. The setup page asks for a **setup token**: `Beacon:UserManagement:SetupToken` when set (at least 32 characters), otherwise a random token Beacon prints **once to the process console (stderr)** at startup, never to the logger. 3. Whoever presents the token creates the first super admin. `POST /beacon/api/setup/superadmin` is anonymous but throttled per IP; once an administrator exists, setup is closed for good. :::caution With several replicas, each one generates its own console token. Set `Beacon:UserManagement:SetupToken` from your secret store for multi-instance deployments, and remove it once setup is done. ::: Until the first administrator exists, SSO and MCP sign-ins cannot provision other users, and every request except setup (`/beacon/api/health` included) is redirected to `/beacon/setup`. ### Headless first run (scripts and CI) The setup token appears on the host's console as `Beacon first-run setup token (enter it on the setup page): `. ```bash BEACON=https://localhost:7187 curl -s $BEACON/beacon/api/setup/status curl -s -X POST $BEACON/beacon/api/setup/superadmin \ -H "Content-Type: application/json" \ -d '{"setupToken":"","userName":"admin","displayName":"Admin","email":"admin@example.com","password":"","confirmPassword":""}' # Sign in (no antiforgery token needed for login); keep the cookie jar for later calls. curl -s -c jar.txt -X POST $BEACON/beacon/api/auth/login \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"","rememberMe":false}' curl -s -b jar.txt $BEACON/beacon/api/auth/me ``` State-changing calls with the cookie need an antiforgery token: `GET /beacon/api/csrf` returns `{"token":"…"}` and sets a cookie; send the token as `X-XSRF-TOKEN` **with the same cookie jar**: ```bash TOKEN=$(curl -s -b jar.txt -c jar.txt $BEACON/beacon/api/csrf | jq -r .token) # A project (you become its ProjectAdmin) … curl -s -b jar.txt -c jar.txt -X POST $BEACON/beacon/api/projects \ -H "Content-Type: application/json" -H "X-XSRF-TOKEN: $TOKEN" \ -d '{"name":"Acme Shop","description":"Shop data"}' # → {"projectId":1,…} # … and an Execute-scoped API key for it (the key is returned once). curl -s -b jar.txt -c jar.txt -X POST $BEACON/beacon/api/api-keys \ -H "Content-Type: application/json" -H "X-XSRF-TOKEN: $TOKEN" \ -d '{"name":"ci-mcp","scopes":["Execute"],"allowedProjectIds":[1]}' # → {"plainTextKey":"sk-sem_…",…} ``` The full request shapes are in the OpenAPI document (`/openapi/v1.json`), once setup is complete. ### Passwords Passwords are hashed with PBKDF2-SHA256 (100,000 iterations, 32-byte salt). Policy is code-configured: `MinimumPasswordLength` (default 8) and `RequirePasswordComplexity` (default `true`). A password an administrator sets must be changed at first sign-in. Users change their own password under **Settings**; there is no REST route that sets another user's password. Password sign-in is throttled to **10 attempts per 60 seconds per client IP** (429 with `Retry-After`). Behind a proxy that only works if your host runs `UseForwardedHeaders` with the proxy listed. See [Production checklist](https://moberghr.github.io/beacon/integrate/production/#reverse-proxy). ### Turning password sign-in off (break-glass) With SSO in place, turn local passwords off and keep up to three named break-glass accounts: ```json { "Beacon": { "UserManagement": { "LocalLogin": { "Enabled": false, "BreakGlassUserNames": [ "beacon-breakglass" ] } } } } ``` - `Enabled: false` unmaps `POST /beacon/api/auth/login` (unless break-glass names exist), hides the password form, and makes every password provider refuse. - `BreakGlassUserNames`: at most three **stored user names**, matched exactly and case-sensitively, enabled and not archived. Each use logs `BreakGlassSignIn` (event 7119, Warning). Treat every occurrence as an incident. - Startup fails if local login is off and nobody else could sign in (no SSO, no external login, no break-glass name). ## OIDC / SSO ```csharp builder.Services.AddBeaconOidcAuthentication(builder.Configuration); ``` ```json { "Beacon": { "Authentication": { "Oidc": { "Enabled": true, "Authority": "https://login.microsoftonline.com//v2.0", "ClientId": "", "ClientSecret": "", "AllowedTenants": [ "" ], "RequiredGroups": [ "" ], "DefaultRoleName": "Viewer", "RequireMfa": true, "DisplayName": "Sign in with Microsoft" } } } } ``` | Key | Default | What it does | |---|---|---| | `Enabled` | `false` | Master switch. When on, startup validates the section. | | `Authority`, `ClientId`, `ClientSecret` | Required | Required when enabled. Code flow with PKCE; tokens are not saved in the cookie. Keep the secret in a secret store. | | `CallbackPath` | `/signin-oidc` | Redirect URI path to register with the identity provider. | | `Scopes` | `openid profile email` | Requested scopes (replaces the list). | | `AllowedTenants` | None | Entra `tid` values that may sign in. **Required** unless `AllowAnyTenant`. | | `AllowAnyTenant` | `false` | Skip the tenant check (identity providers without `tid`). Warns at startup with an Entra authority. | | `BlockGuests` | `true` | Refuse Entra B2B guests (`acct=1`, or `idp` naming another provider). | | `RequiredGroups` / `RequiredRoles` | None | When either is set, the user needs at least one listed group object id or app role. | | `DefaultRoleName` | none | Role given to a user created at first SSO sign-in. Unset: new users get **no** role until someone grants one. Cannot be `Admin` or a functional role. | | `RequireMfa` | `false` | Refuse sign-ins whose ID token has no `amr` containing `mfa` and no `acr` listed in `AcceptedAcrValues`. | | `AcceptedAcrValues` | None | `acr` values that count as MFA. | | `EnableFrontChannelLogout` | `false` | Serve `/signout-oidc` for identity-provider-initiated logout. | | `DisplayName` | `SSO` | Label of the SSO button. | Admission runs **before** any user lookup: tenant → guest block → required group or role. MFA covers the interactive SSO sign-in only. It does not apply to bearer tokens, API keys, MCP callers or break-glass passwords. Entra app roles can also grant Beacon's [functional permissions](https://moberghr.github.io/beacon/integrate/access-control/#3-functional-permissions) at sign-in (`Beacon:Authorization:IdpRoles`). ## Bearer JWTs Bearer access tokens work for the REST API and for MCP. They are configured in code: ```csharp builder.Services.AddBeaconJwtAuthentication(jwt => { jwt.EnableBearerAuthentication = true; jwt.Validation.JwksEndpoint = "https://login.microsoftonline.com//discovery/v2.0/keys"; jwt.Validation.ValidIssuers = [ "https://login.microsoftonline.com//v2.0", "https://sts.windows.net//" ]; jwt.Validation.ValidAudiences = [ "api://", "" ]; }); // … app.UseBeaconJwtBearerAuthentication(); // in the pipeline, after ApiKeyAuthMiddleware ``` | Option | Default | Notes | |---|---|---| | `EnableBearerAuthentication` | `false` | Turns the middleware on. | | `Validation.JwksEndpoint` or `Validation.SigningKey` | Required | One is required. | | `Validation.ValidIssuer(s)`, `ValidAudience(s)` | Required | At least one of each; issuer, audience and lifetime validation must stay on. | | `Validation.ClockSkew` | 5 min | At most 5 minutes. | | `AccessTokenClaim` | None | For non-Entra providers: a claim that proves the token is an access token (ID-token claims such as `sub` or `nonce` are refused). | | `ClaimsMapping` | `sub`, `preferred_username`, `email`, `roles`, `name` | Which claims carry the user id, user name, email, roles and display name. | | `ExternalLoginEndpoint` | None | Let the login form validate credentials against your own token endpoint. | A token must prove it is an **access** token (Entra: `azp`/`appid` plus `scp` or `roles`; others: `typ: at+jwt` or `AccessTokenClaim`) and must bind to an existing, enabled Beacon user, so bearer tokens on the REST API need Beacon's user management. On `/beacon/mcp`, tokens go through the **MCP caller mapping** instead (`Beacon:Mcp:Callers`). See [MCP & AI clients](https://moberghr.github.io/beacon/integrate/mcp/#option-b-entra-id-callers). ## API keys Users create API keys under **API keys** (`/beacon/api-keys`). A key: - is `sk-sem_` followed by about 43 random base64 characters, shown **once**, stored as a SHA-256 hash; - has the **Read** or **Execute** scope (Read: reads only; Execute: everything the owner may do, including running SQL and MCP); - names **at least one project** the owner can read, and reaches only those; - expires: 90 days by default, at most `Beacon:ApiKeys:MaxLifetimeDays` (365); - acts as its owner, capped by the lower of its scope and the owner's current role, never with the Admin bypass and never with a functional permission; - stops working when its owner is disabled or archived. Send it as `Authorization: Bearer sk-sem_…`. Details: [API keys](https://moberghr.github.io/beacon/features/api-keys/). ## Sessions | Setting | Default | What it does | |---|---|---| | `Beacon:Authentication:Session:RevalidateMinutes` | `5` | How often a session is re-checked against the user store (user enabled, not archived, unchanged security stamp; roles reloaded). `0` checks every request. | | `Beacon:Authentication:Session:AbsoluteLifetimeHours` | `12` | A session ends this long after sign-in, however often it is renewed. "Remember me" cannot extend it. | The `Beacon.Auth` cookie is HttpOnly, SameSite=Lax and Secure on HTTPS. Changing a user's roles, password, enabled state or archiving them renews their **security stamp**, which ends every session they have. Users can sign out everywhere themselves; a UserAdmin can do it for them (`POST /beacon/api/users/{id}/sign-out-everywhere`). Realtime connections close when the session ends. ## Your app owns sign-in When Beacon runs inside your product and your users already sign in to it: 1. Leave `EnableUserManagement()` and `EnableLoginForm` off. 2. Register your authentication scheme as usual; keep `AddBeaconCookieAuthentication("/beacon")` only if you also want the Beacon cookie. 3. Make your principal carry `ClaimTypes.NameIdentifier` and a role claim of `Admin`, `Editor` or `Viewer` (your claims transformation can map your roles). 4. Tell the UI where to send signed-out users: ```csharp builder.Services.AddBeaconApiServices(x => x.ExternalLogin = new ExternalLoginOptions { LoginUrl = "/account/login" }); ``` Without Beacon's user store: - project membership comes from an `IProjectMembershipSource` you register, or projects stay open; - only the `Admin` role claim grants functional permissions, unless `Beacon:Authorization:Roles:HonourHostRoleClaims` is `true`, in which case a role claim named like a permission's role grants it. ## Choosing a combination | Deployment | Recommended | |---|---| | Internal tool, small team | User store + login form; one break-glass account. | | Company with Entra ID | User store + OIDC (`AllowedTenants`, `RequiredGroups`, `RequireMfa`), local login off with one break-glass name, IdP app roles for functional permissions. | | Embedded in your SaaS | Your sign-in + `ExternalLogin` + a membership source; Beacon cookie optional. | | AI clients | API keys per person (Execute scope, one project each), or Entra callers for organisation-wide MCP access. | --- # Access control > Decide who sees what before go-live: global roles, projects as the access boundary, functional permissions, separation of duties, IdP role mapping, change approvals and deployment locks. Source: https://moberghr.github.io/beacon/integrate/access-control/ Beacon's access model has four layers. Set each one deliberately before people start using it. ```mermaid flowchart TB R["Global role
Admin · Editor · Viewer
the ceiling"] --> P["Project membership
ProjectAdmin · Editor · Viewer
what you can touch"] P --> A["Project action
Read · Run · Query · Write · Manage · ManageMembers · Curate"] F["Functional permissions
Approver · Auditor · SecurityAdmin
UserAdmin · DataSourceAdmin · MigrationOperator
duties, no data"] -.-> X["Admin screens and decisions"] L["Deployment locks
config only, bind Admins too"] -.-> A ``` ## 1. Global roles Every user has at most one global role, which caps everything else: | Role | Can | |---|---| | **Viewer** | Read, and run existing checks, in projects they can reach. | | **Editor** | Also create and change queries, subscriptions and other project content. | | **Admin** | Everything, in every project, plus every functional permission. | The first super admin is created at [first-run setup](https://moberghr.github.io/beacon/integrate/authentication/#beacons-user-store-and-first-run-setup). Give `Admin` to as few people as possible. The functional permissions below exist so that most administrative duties do not need it. ## 2. Projects are the access boundary A **project** groups data sources and the queries, subscriptions, contracts and grounding built on them. Access is decided per project by one component, `IProjectAccessEvaluator`: - **Restricted** projects are visible only to their **members** and to Admins. Everyone else gets **404**, so the existence of the project is not disclosed. - **Open** projects are visible to everyone with a global role, at that role. - **New projects start restricted**, with their creator as **ProjectAdmin**. - A **data source follows the projects that link it**. Something that reads several data sources is reachable only through one project that links all of them. Data sources created after project access was introduced are Admin-only until linked to a project. | Project role | Actions | |---|---| | Viewer | Read, Run | | Editor | + Query (ad-hoc SQL, MCP queries), Write; + Manage in open projects | | ProjectAdmin | + Manage, ManageMembers, Curate (review learned patterns, documentation and relationships) | A global **Viewer** is a Viewer in every project, whatever their membership; Editors and Admins act with their membership role (and in an open project, with the higher of that and their global role). `ManageMembers` and `Curate` come only from a ProjectAdmin membership and are never granted to API keys, MCP callers or scheduled work. **Make every project restricted** with `Beacon:Authorization:RequireProjectMembership: true`. Startup then requires a membership source (Beacon's user store, or your `IProjectMembershipSource`). Plan the first projects before inviting users: 1. Create one project per team or data domain. 2. Link each data source to the projects that should see it. 3. Add members with the lowest role that works. ## 3. Functional permissions Administrative duties are split out of the Admin role. Each is held through a **functional role** of the same name (a system role with no data access), assigned by an Admin, or mapped from an identity-provider app role at SSO sign-in. | Permission | Grants | |---|---| | **Approver** | Approve or reject query changes and change requests (migration jobs, Custom SQL rules, recipient destinations). | | **Auditor** | Read and verify the [security audit trail](https://moberghr.github.io/beacon/features/security-audit/); read the MCP audit log, MCP settings, admin settings, every API key and what a user owns, created or changed last. | | **SecurityAdmin** | Change MCP and admin settings (not the LLM connection or base URL, which are Admin only), manage notification recipients, publish saved queries as MCP tools, set repository tokens. | | **UserAdmin** | Manage users and their non-privileged roles, end sessions, see what a user owns, created or changed last, revoke API keys, change owners of scheduled work, manage project members, restrict projects. | | **DataSourceAdmin** | Create, test and delete data sources; link them to projects they can read. | | **MigrationOperator** | Create and run data-migration jobs (only when migrations are enabled). | A functional permission grants **no project data**. A UserAdmin can never grant Admin or a functional role, nor touch Admins or other privileged users. API keys never hold a functional permission. ```json { "Beacon": { "Authorization": { "RequireProjectMembership": true, "Roles": { "Approver": "Beacon Approver" }, "IdpRoles": { "Enabled": true, "Map": { "Beacon.Auditor": "Auditor", "Beacon.SecurityAdmin": "SecurityAdmin" }, "ClaimLifetimeMinutes": 60 }, "SeparationOfDuties": { "Enabled": true } } } } ``` | Key | Default | What it does | |---|---|---| | `Roles:{Permission}` | the permission's name | Rename the functional role that grants a permission (not Admin/Editor/Viewer; unique). | | `Roles:HonourHostRoleClaims` | `false` | Without Beacon's user store: let a role claim named like a functional role grant the permission. | | `IdpRoles:Enabled` / `Map` | off | Map identity-provider app roles to permissions at SSO sign-in. Mapped permissions live in the session only and are not stored. | | `IdpRoles:ClaimLifetimeMinutes` | `60` | How long a mapped permission counts after sign-in. | | `SeparationOfDuties:Enabled` | `false` | Refuse giving one person both halves of a conflicting pair: Approver + SecurityAdmin, Auditor + SecurityAdmin, Auditor + UserAdmin, UserAdmin + Approver, UserAdmin + MigrationOperator. | ## 4. Change approvals (four-eyes) Opt in with `options.EnableApprovalWorkflow(...)`. Then a change to a query's executable content goes live only after a **second person** with the Approver permission approves exactly the content they reviewed, and scheduled runs execute only reviewed SQL. By default it also covers migration jobs, Custom SQL rules and new or changed notification destinations. ```csharp options.EnableApprovalWorkflow(x => { x.RequireApprovedVersionToRun = true; // strict: run only versions with an approval for their exact content // x.AllowSelfApproval = true; // only for a deployment with a single possible approver // x.CoversMigrationJobs = false; // per-kind switches, all true by default }); ``` Make sure at least two people hold Approver (Admins count). Full rules: [Approvals](https://moberghr.github.io/beacon/features/approvals/). ## 5. Deployment locks Some guarantees should not depend on an individual Admin's actions. These keys can only be changed in host configuration. The UI shows the setting disabled, and the API answers 409: | Key | Default | Pins | |---|---|---| | `Beacon:Mcp:ForceReadOnly` | `true` | Read-only SQL everywhere the gate runs. | | `Beacon:Mcp:ForcePiiDetection` | `true` | PII detection and masking on. | | `Beacon:Mcp:ForceNoContentRetention` | `false` | No question, SQL or error text stored with MCP audit rows. | | `Beacon:Mcp:ForbidRowDataToLlm` | `false` | No row values (samples, value grounding, eval judging) sent to the LLM provider. | | `Beacon:Mcp:Audit:RequireContent` | `false` | Content retention on: MCP audit rows keep the question and the SQL of every statement a call ran. Cannot be combined with `ForceNoContentRetention`. | | `Beacon:Mcp:Ceilings:MaxRowLimit` | none | Upper bound for every project's MCP row limit. | | `Beacon:Migrations:Enabled` | `false` | The only write path. Off: no migration endpoints, no jobs, for anyone. | See [Security model](https://moberghr.github.io/beacon/features/security-model/) for how these fit together, and [Authorization](https://moberghr.github.io/beacon/features/authorization/) for the full reference. ## A starting configuration For a typical company deployment: - `RequireProjectMembership: true` - SSO with `AllowedTenants`, `RequiredGroups`, `RequireMfa`; local login off with one break-glass account - Two or more Approvers, one Auditor, one SecurityAdmin; `SeparationOfDuties` on - Approval workflow on, with `RequireApprovedVersionToRun` - Locks left at their defaults; `ForbidRowDataToLlm` on if row values must never reach your LLM provider - Migrations off unless you need them - `Beacon:Audit:IntegrityKey` set from a secret store, and the security audit verification and retention jobs scheduled --- # Background jobs > Beacon runs no background work of its own; your job runner does. The IBeaconScheduler contract, the jobs Beacon expects, the system-work rule, a complete Warp implementation, and a Hangfire sketch. Source: https://moberghr.github.io/beacon/integrate/background-jobs/ Scheduled checks, data-quality evaluations, documentation generation, AI actor cycles and maintenance all run as background jobs. Beacon defines **what** must run; your host decides **how**, through one interface: `IBeaconScheduler`. ```mermaid sequenceDiagram participant Beacon as Beacon (handlers) participant Sched as Your IBeaconScheduler participant Runner as Your job runner participant Jobs as IJobService (Beacon) Beacon->>Sched: AddOrUpdate(subscriptionId, name, cron) Sched->>Runner: register recurring job "subscription-{id}" Note over Runner: cron fires Runner->>Runner: new DI scope, mark as system work Runner->>Jobs: ExecuteQuery(subscriptionId) Jobs->>Jobs: run as the subscription's owner, re-check access, gate SQL, notify ``` ## The contract ```csharp namespace Beacon.Core.Worker; public interface IBeaconScheduler { Task AddOrUpdate(int subscriptionId, string subscriptionName, string cron); Task Remove(int subscriptionId, string subscriptionName); // The members below have default bodies that throw NotImplementedException. // Implement them to enable the feature that calls them. Task AddOrUpdateDataQualityJob(int contractId, string contractName, string cron); // data contracts Task RemoveDataQualityJob(int contractId, string contractName); Task EnqueueProjectDocumentation(int projectId, int userId, string? notifyUserId); // AI documentation Task EnqueueAiActorThinkCycle(int actorId, int subscriptionId); // AI actors Task EnqueueMcpEval(int runId); // MCP evaluation runs } ``` Register it with `options.AddBeaconScheduler()`. Without one, `AddBeaconServices` throws `Implementation of IBeaconScheduler is required.` - `cron` is a standard **5-field** expression (validated by Beacon before it reaches you). - Key recurring jobs by the **entity id** (`subscription-{id}`, `dq-{id}`), never by the name. Names change, ids do not. - The `Enqueue…` methods return your runner's job id as a string. The UI correlates live `JobStatusChanged` events by that id; `notifyUserId` is the user who should receive them. ## What each job calls Each job opens a **new DI scope**, marks it as system work, and calls one Beacon method: | Job | Calls | Scheduled by | |---|---|---| | Subscription run | `IJobService.ExecuteQuery(subscriptionId, ct)` | `AddOrUpdate` / `Remove` | | Data contract evaluation | `IJobService.EvaluateDataContract(contractId, ct)` | `AddOrUpdateDataQualityJob` / `RemoveDataQualityJob` | | Project documentation | `IProjectDocumentationService.GenerateDocumentationAsync(projectId, userId, ct)` (Beacon.AI) | `EnqueueProjectDocumentation` | | AI actor think cycle | `IAiActorServiceExtended.ExecuteThinkCycleBackgroundAsync(actorId, subscriptionId)` (Beacon.AI) | `EnqueueAiActorThinkCycle` | | MCP evaluation run | `IJobService.RunMcpEval(runId, ct)` | `EnqueueMcpEval` | ### Recurring maintenance jobs Register these yourself at startup (the reference host's schedule): | Id | Cron | Calls | Purpose | |---|---|---|---| | `mcp-learning-aggregate` | `0 */6 * * *` | `IJobService.AggregateLearnedPatterns` | Mine MCP usage signals into learned patterns. | | `mcp-learning-cleanup` | `0 3 * * *` | `IJobService.CleanupOldSignals` | Delete signals past their retention. | | `mcp-audit-retention` | `30 3 * * *` | `IMcpAuditRetentionService.PurgeExpiredAsync` | Purge MCP audit rows older than `Beacon:Mcp:Audit:RetentionDays` (no-op when unset). | | `mcp-embedding-reindex` | `0 */12 * * *` | `IJobService.ReindexEmbeddings` | Re-embed metadata and examples for semantic retrieval. | | `mcp-docchunk-reindex` | `0 */12 * * *` | `IJobService.ReindexDocChunks` | Re-chunk and re-embed project documentation. | One-off operator jobs: - While `Beacon:PreviousEncryptionKeys` lists a key, run `IStoredValueReEncryptionService.ReEncryptAsync` once per start (the reference host checks `Beacon.Core.ServiceConfiguration.HasPreviousEncryptionKeys(configuration)`). Remove the old key when it reports nothing left. - `IRecipientSecretEncryptionService.EncryptStoredSecretsAsync` encrypts recipient secrets stored before encryption existed. Run it once, by hand, after **every** instance runs the new version. ## The system-work rule Beacon decides project access from the caller. A background job has no HTTP request, so it must say that it is Beacon's own work: ```csharp scope.ServiceProvider.GetRequiredService().MarkAsSystemWork(); // Beacon.Core.Authorization ``` Do this in the job's scope **before** Beacon code runs. An unmarked scope without a request gets **no project access** and logs a warning, and the job does nothing useful. Work done for a user (a subscription's scheduled run) is still executed **as that user**: Beacon sets the run-as owner itself and re-checks their access to the exact data sources right before any SQL or LLM call. A run the owner may no longer do is recorded as skipped and never run as the system. Do not resolve the current user in your job wrapper before Beacon's method runs. ## Reference implementation: Moberg Warp [Moberg Warp](https://moberghr.github.io/warp/) is a PostgreSQL-backed job runner with recurring jobs and a dashboard. The reference host (`src/Beacon.SampleProject`) uses Warp 3.11; the code below matches it. Check the Warp docs if you use a newer major version. ```bash dotnet add package Moberg.Warp.Core --version 3.11.0 dotnet add package Moberg.Warp.Worker --version 3.11.0 dotnet add package Moberg.Warp.Provider.PostgreSql --version 3.11.0 dotnet add package Moberg.Warp.UI --version 3.11.0 # optional dashboard dotnet add package EFCore.NamingConventions # UseSnakeCaseNamingConvention dotnet add package Microsoft.EntityFrameworkCore.Design # for dotnet ef (same EF Core 10 version) dotnet tool install --global dotnet-ef ``` **1. A dedicated context for Warp's tables** (the `warp` schema), kept apart from Beacon's: ```csharp using Microsoft.EntityFrameworkCore; public sealed class WarpDbContext(DbContextOptions options) : DbContext(options); ``` Create its initial migration once in your project: `dotnet ef migrations add WarpInitial --context WarpDbContext`. Warp supplies the model. **2. Registration and startup:** ```csharp using Microsoft.EntityFrameworkCore; using Warp.Core; using Warp.Provider.PostgreSql; using Warp.Worker; public static class BeaconWarpSetup { public static IServiceCollection AddBeaconWarp(this IServiceCollection services, IConfiguration configuration) { // Warp supports PostgreSQL; with Beacon on SQL Server, point WarpContext at a PostgreSQL database. services.AddDbContext(x => x .UseNpgsql(configuration.GetConnectionString("WarpContext") ?? configuration.GetConnectionString("BeaconContext")) .UseSnakeCaseNamingConvention()); services.AddWarpServer(x => { x.UsePostgreSql(); x.PollingInterval = TimeSpan.FromSeconds(1); x.WorkerCount = Math.Min(Environment.ProcessorCount * 2, 20); // protects the connection pool // No x.AddRetry(): failed Beacon jobs are investigated, not retried blindly. }); return services; } public static async Task StartBeaconWarpAsync(this WebApplication app) { using var scope = app.Services.CreateScope(); scope.ServiceProvider.GetRequiredService().Database.Migrate(); var recurring = scope.ServiceProvider.GetRequiredService(); await recurring.AddOrUpdateRecurringJob(new AggregateLearnedPatternsJob(), "mcp-learning-aggregate", "0 */6 * * *"); await recurring.AddOrUpdateRecurringJob(new CleanupOldSignalsJob(), "mcp-learning-cleanup", "0 3 * * *"); await recurring.AddOrUpdateRecurringJob(new PurgeExpiredMcpAuditLogsJob(), "mcp-audit-retention", "30 3 * * *"); await recurring.AddOrUpdateRecurringJob(new ReindexEmbeddingsJob(), "mcp-embedding-reindex", "0 */12 * * *"); await recurring.AddOrUpdateRecurringJob(new ReindexDocChunksJob(), "mcp-docchunk-reindex", "0 */12 * * *"); } } ``` **3. Mark every job as system work** with a pipeline behaviour (Warp's source generator registers it): ```csharp using Beacon.Core.Authorization; using Warp.Core.Handlers; public sealed class SystemWorkBehavior(ISystemWorkScope systemWork) : IPipelineBehavior where TRequest : IRequest { public Task HandleAsync(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) { systemWork.MarkAsSystemWork(); return next(request, cancellationToken); } } ``` **4. One job + handler per Beacon entry point:** ```csharp using Beacon.Core.Services.Retention; using Beacon.Core.Worker; using Warp.Core.Handlers; public sealed class ExecuteSubscriptionQueryJob : IJob { public int SubscriptionId { get; init; } } public sealed class ExecuteSubscriptionQueryJobHandler(IJobService jobs) : IJobHandler { public Task HandleAsync(ExecuteSubscriptionQueryJob message, CancellationToken cancellationToken) => jobs.ExecuteQuery(message.SubscriptionId, cancellationToken); } public sealed class EvaluateDataContractJob : IJob { public int ContractId { get; init; } } public sealed class EvaluateDataContractJobHandler(IJobService jobs) : IJobHandler { public Task HandleAsync(EvaluateDataContractJob message, CancellationToken cancellationToken) => jobs.EvaluateDataContract(message.ContractId, cancellationToken); } public sealed class RunMcpEvalJob : IJob { public int RunId { get; init; } } public sealed class RunMcpEvalJobHandler(IJobService jobs) : IJobHandler { public Task HandleAsync(RunMcpEvalJob message, CancellationToken cancellationToken) => jobs.RunMcpEval(message.RunId, cancellationToken); } public sealed class AggregateLearnedPatternsJob : IJob; public sealed class AggregateLearnedPatternsJobHandler(IJobService jobs) : IJobHandler { public Task HandleAsync(AggregateLearnedPatternsJob message, CancellationToken cancellationToken) => jobs.AggregateLearnedPatterns(cancellationToken); } public sealed class CleanupOldSignalsJob : IJob; public sealed class CleanupOldSignalsJobHandler(IJobService jobs) : IJobHandler { public Task HandleAsync(CleanupOldSignalsJob message, CancellationToken cancellationToken) => jobs.CleanupOldSignals(cancellationToken); } public sealed class ReindexEmbeddingsJob : IJob; public sealed class ReindexEmbeddingsJobHandler(IJobService jobs) : IJobHandler { public Task HandleAsync(ReindexEmbeddingsJob message, CancellationToken cancellationToken) => jobs.ReindexEmbeddings(cancellationToken); } public sealed class ReindexDocChunksJob : IJob; public sealed class ReindexDocChunksJobHandler(IJobService jobs) : IJobHandler { public Task HandleAsync(ReindexDocChunksJob message, CancellationToken cancellationToken) => jobs.ReindexDocChunks(cancellationToken); } public sealed class PurgeExpiredMcpAuditLogsJob : IJob; public sealed class PurgeExpiredMcpAuditLogsJobHandler(IMcpAuditRetentionService retention) : IJobHandler { public Task HandleAsync(PurgeExpiredMcpAuditLogsJob message, CancellationToken cancellationToken) => retention.PurgeExpiredAsync(cancellationToken); } ``` With AI enabled (`UseAI = true`), add the two AI jobs too: ```csharp using Beacon.AI.Services.Ai.AiActor; // IAiActorServiceExtended using Beacon.AI.Services.Documentation; // IProjectDocumentationService using Warp.Core.Handlers; public sealed class GenerateProjectDocumentationJob : IJob { public int ProjectId { get; init; } public int UserId { get; init; } } public sealed class GenerateProjectDocumentationJobHandler(IProjectDocumentationService documentation) : IJobHandler { public Task HandleAsync(GenerateProjectDocumentationJob message, CancellationToken cancellationToken) => documentation.GenerateDocumentationAsync(message.ProjectId, message.UserId, cancellationToken); } public sealed class AiActorThinkCycleJob : IJob { public int ActorId { get; init; } public int SubscriptionId { get; init; } } public sealed class AiActorThinkCycleJobHandler(IAiActorServiceExtended actors) : IJobHandler { public Task HandleAsync(AiActorThinkCycleJob message, CancellationToken cancellationToken) => actors.ExecuteThinkCycleBackgroundAsync(message.ActorId, message.SubscriptionId); } ``` The reference host's versions are in `src/Beacon.SampleProject/Warp/Jobs/`. **5. The scheduler:** ```csharp using Beacon.Core.Worker; using Microsoft.EntityFrameworkCore; using Warp.Core; using Warp.Core.Data.Entities; public sealed class BeaconScheduler( IPublisher publisher, IRecurringJobPublisher recurring, WarpDbContext warp) : IBeaconScheduler { public Task AddOrUpdate(int subscriptionId, string subscriptionName, string cron) => recurring.AddOrUpdateRecurringJob(new ExecuteSubscriptionQueryJob { SubscriptionId = subscriptionId }, $"subscription-{subscriptionId}", cron); public Task Remove(int subscriptionId, string subscriptionName) => RemoveRecurring($"subscription-{subscriptionId}"); public Task AddOrUpdateDataQualityJob(int contractId, string contractName, string cron) => recurring.AddOrUpdateRecurringJob(new EvaluateDataContractJob { ContractId = contractId }, $"dq-{contractId}", cron); public Task RemoveDataQualityJob(int contractId, string contractName) => RemoveRecurring($"dq-{contractId}"); public async Task EnqueueMcpEval(int runId) { var id = await publisher.Enqueue(new RunMcpEvalJob { RunId = runId }); await publisher.SaveChangesAsync(); return id.ToString(); } public async Task EnqueueProjectDocumentation(int projectId, int userId, string? notifyUserId) { var id = await publisher.Enqueue(new GenerateProjectDocumentationJob { ProjectId = projectId, UserId = userId }); await publisher.SaveChangesAsync(); return id.ToString(); } public async Task EnqueueAiActorThinkCycle(int actorId, int subscriptionId) { var id = await publisher.Enqueue(new AiActorThinkCycleJob { ActorId = actorId, SubscriptionId = subscriptionId }); await publisher.SaveChangesAsync(); return id.ToString(); } private async Task RemoveRecurring(string name) { var job = await warp.Set() .Where(x => x.Name == name) .FirstOrDefaultAsync(); if (job == null) { return; } warp.Set().Remove(job); await warp.SaveChangesAsync(); } } ``` **6. The dashboard (optional).** Keep it Admin-only: ```csharp app.UseWarpUI(x => { x.RoutePrefix = "/warp"; x.Authorization = new AdminOnlyWarpFilter(); // IWarpAuthorizationFilter: authenticated + IsInRole("Admin") }); ``` The filter reads the Beacon session, which applies to `/beacon` and the prefixes in `Beacon:Authentication:AdditionalPaths` only: list `/warp` there (`"Beacon": { "Authentication": { "AdditionalPaths": [ "/warp" ] } }`), or the dashboard refuses every request. ### Live job status in the UI The reference host also pushes `JobStatusChanged` (Processing / Succeeded / Failed) to the user who started a documentation job, through the SignalR hub. It tags the job with `BeaconUserId` metadata and a second pipeline behaviour sends the event; see `src/Beacon.SampleProject/Warp/JobStatusChangedBehavior.cs`. Without it everything works; the UI just does not show live progress for those jobs. ## Other job runners Any runner works if it can (1) run a job in its own DI scope, (2) register, update and remove recurring jobs by name, and (3) enqueue a one-off job and return an id. A Hangfire sketch: wrap Beacon's calls in a class that marks system work first: ```csharp using Beacon.Core.Authorization; using Beacon.Core.Worker; using Hangfire; public sealed class BeaconHangfireJobs(ISystemWorkScope systemWork, IJobService jobs) { public Task ExecuteSubscription(int subscriptionId) { systemWork.MarkAsSystemWork(); return jobs.ExecuteQuery(subscriptionId, CancellationToken.None); } public Task EvaluateContract(int contractId) { systemWork.MarkAsSystemWork(); return jobs.EvaluateDataContract(contractId, CancellationToken.None); } } public sealed class HangfireBeaconScheduler(IRecurringJobManager recurring) : IBeaconScheduler { public Task AddOrUpdate(int subscriptionId, string subscriptionName, string cron) { recurring.AddOrUpdate($"subscription-{subscriptionId}", x => x.ExecuteSubscription(subscriptionId), cron); return Task.CompletedTask; } public Task Remove(int subscriptionId, string subscriptionName) { recurring.RemoveIfExists($"subscription-{subscriptionId}"); return Task.CompletedTask; } } ``` Hangfire activates `BeaconHangfireJobs` in a scope per job when you use its ASP.NET Core integration. Turn its automatic retries off for these jobs (`[AutomaticRetry(Attempts = 0)]`), or a failed check reruns and notifies again. ## Operating notes - **No automatic retries.** A failed run is recorded and visible in the UI; investigate it rather than retrying blindly. If one of your own jobs needs retries, configure them on that job only. - **Worker count vs connection pool.** Each subscription run opens a Beacon context plus a connection to the monitored database. Cap workers so you do not exhaust the pool (the reference host uses `min(2 × CPU, 20)`). - **Several instances.** Your runner must make sure a recurring job fires once across the cluster. Warp and Hangfire do this through their database. - **A worker host without `Beacon.Api`.** A service that runs Beacon's jobs but serves no Beacon endpoints (a .NET Worker host referencing `Moberg.Beacon.Core` and a database provider only) keeps Core's request principal (`IBeaconRequestPrincipal`), which serves no request; every scope there runs off-request, so mark each job's scope as system work or set the owner it runs as. `Beacon.Api` registers `IBeaconUserContext`; such a host registers its own. --- # Data-source connectors > Register the connectors for the engines Beacon monitors, what each one supports, how read-only is enforced per engine, and how to create the read-only logins Beacon should use. Source: https://moberghr.github.io/beacon/integrate/connectors/ A **connector** teaches Beacon one kind of data source. Register the ones you need on the `AddBeaconServices(...)` builder, before `UsePostgreSql()` / `UseSqlServer()`: ```csharp using Beacon.Connector.PostgreSql; using Beacon.Connector.SqlServer; builder.Services.AddBeaconServices(builder.Configuration, options => { /* … */ }) .AddPostgreSqlConnector() .AddSqlServerConnector() .UsePostgreSql(); ``` | Engine | Package | Registration | |---|---|---| | PostgreSQL | `Moberg.Beacon.Connector.PostgreSql` | `.AddPostgreSqlConnector()` | | SQL Server | `Moberg.Beacon.Connector.SqlServer` | `.AddSqlServerConnector()` | | MySQL | `Moberg.Beacon.Connector.MySql` | `.AddMySqlConnector()` | | Azure Synapse | `Moberg.Beacon.Connector.AzureSynapse` | `.AddAzureSynapseConnector()` | | Snowflake | `Moberg.Beacon.Connector.Snowflake` | `.AddSnowflakeConnector()` | | Google BigQuery | `Moberg.Beacon.Connector.BigQuery` | `.AddBigQueryConnector()` | | Databricks | `Moberg.Beacon.Connector.Databricks` | `.AddDatabricksConnector()` | | AWS CloudWatch Logs | `Moberg.Beacon.Connector.CloudWatch` | `.AddCloudWatchConnector()` | | REST API (OpenAPI) | `Moberg.Beacon.Connector.Api` | `.AddApiConnector()` | Only registered engines appear in **New data source**. Data sources themselves (connection details, metadata options, project links) are created in the UI by a DataSourceAdmin or an Admin, and their connection strings are encrypted with `Beacon:EncryptionKey` before they are stored. See [Data sources](https://moberghr.github.io/beacon/features/data-sources/). ## What each engine supports | Engine | Metadata scan | Database-level read-only | Row cap stops the read early | Migration write target | |---|---|---|---|---| | PostgreSQL | Yes | **Yes**: `READ ONLY` transaction, one statement per batch | Yes | Yes | | MySQL | Yes | **Yes**: `START TRANSACTION READ ONLY`, batches off | Yes | Yes | | SQL Server | Yes | No: runs in a transaction that is always rolled back | Yes | Yes | | Azure Synapse | Yes | No | Yes | No | | Snowflake | Yes | No | Yes | No | | BigQuery | No | No: the connector's own parser check plus a dry run whose statement type must be `SELECT` | No (reads the capped result) | No | | Databricks | No | No: the connector's own parser check and configured catalog | No (reads the capped result) | No | | CloudWatch | Log fields | n/a (Logs Insights queries, no SQL) | n/a | No | | REST API | OpenAPI endpoints | n/a (HTTP requests, no SQL) | n/a | No | Every SQL engine goes through the same [execution gate](https://moberghr.github.io/beacon/features/security-model/#6-the-sql-execution-gate) before anything runs: length and character checks, a guardrail, a dialect-aware AST validator, the cross-database rule, the refusal of the engine's catalog, statistics and query history, a row cap written into the SQL and a statement timeout. Where the database cannot refuse a write itself, the data source shows a badge and Beacon logs a warning at startup and on save. :::caution[Always use a read-only login] The gate is the main defence; database-level read-only is defence in depth. Give every data source a login that **can only read**, on every engine, including PostgreSQL and MySQL. A data-migration write target is the one exception, and it is a separate, Admin-marked data source. ::: ## Creating read-only logins **PostgreSQL** ```sql CREATE ROLE beacon_reader LOGIN PASSWORD '…'; GRANT CONNECT ON DATABASE shop TO beacon_reader; GRANT USAGE ON SCHEMA public TO beacon_reader; GRANT SELECT ON ALL TABLES IN SCHEMA public TO beacon_reader; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO beacon_reader; ALTER ROLE beacon_reader SET default_transaction_read_only = on; ``` **SQL Server** ```sql CREATE LOGIN beacon_reader WITH PASSWORD = '…'; USE shop; CREATE USER beacon_reader FOR LOGIN beacon_reader; ALTER ROLE db_datareader ADD MEMBER beacon_reader; DENY INSERT, UPDATE, DELETE, ALTER, EXECUTE TO beacon_reader; ``` **MySQL** ```sql CREATE USER 'beacon_reader'@'%' IDENTIFIED BY '…'; GRANT SELECT, SHOW VIEW ON shop.* TO 'beacon_reader'@'%'; ``` **Snowflake, Synapse, BigQuery, Databricks:** a role or service principal with read access to the objects Beacon should see, and nothing else (Snowflake: a role with `USAGE` + `SELECT`; BigQuery: `BigQuery Data Viewer` + `BigQuery Job User` on the project; Databricks: `SELECT` + `USE CATALOG`/`USE SCHEMA` through Unity Catalog). ## Connection details per engine | Engine | The **New data source** dialog asks for | |---|---| | PostgreSQL, SQL Server, MySQL, Synapse, Snowflake | Database engine and connection string, plus metadata options: max tables, max columns per table, include and exclude schemas | | Databricks | Host, HTTP path, personal access token; catalog and schema (optional) | | BigQuery | Project id, service-account JSON; dataset id and location (optional) | | CloudWatch | AWS region, log groups; access key id, secret access key or AWS profile (optional) | | REST API | Base URL, OpenAPI spec URL, authentication (API key in a header or parameter, bearer token, or basic), include and exclude path patterns | The last step of the dialog runs **Test connection** before **Create data source**. Queries may only name objects in the database (catalog, project) the data source is configured for: the gate refuses other databases, remote servers, files and dynamic SQL. ## Cross-database queries A query can have several steps on different data sources. Each step's result is materialised (`@result1`, `@result2`, …) and a final query joins them in an in-memory SQLite database. Nothing is joined across engines at the database level, and the final query passes the same gate. See [Queries](https://moberghr.github.io/beacon/features/queries/). ## Monitoring your own application's database To let Beacon (and MCP clients) read your application's own EF Core model (with a table allow-list, column masking and a read-only connection string that is never stored) use `ExposeDbContext(...)` instead of creating a data source by hand. See [Extending Beacon](https://moberghr.github.io/beacon/integrate/extending/#your-dbcontext-as-a-data-source). ## Notes - Connector registration also fills process-wide registries (engine → connection factory). Register connectors once, at startup. - The data-quality SQL generator supports PostgreSQL, SQL Server and MySQL. --- # AI providers > Turn on Beacon's AI layer: UseAI and AddBeaconAI, the four LLM providers and their settings, the request queue, runtime switching, local embeddings for semantic retrieval, and what data reaches the provider. Source: https://moberghr.github.io/beacon/integrate/ai/ Beacon's AI layer powers natural-language questions (`ask`), project documentation, grounding and the learning loop, and AI actors. It is optional, except that the MCP package depends on the AI package, so an MCP host always registers it. ## Turning it on ```csharp builder.Services.AddBeaconServices(builder.Configuration, options => { options.UseAI = true; // … }); builder.Services.AddBeaconAI(builder.Configuration); // binds Beacon:LLM, registers providers and the queue ``` Set both. `UseAI = false` registers stand-ins for the AI-only services; `UseAI = true` without `AddBeaconAI` leaves those services unregistered. ## Choosing a provider Configure the provider in host configuration (`Beacon:LLM`). At startup Beacon builds the provider from that section. An Admin can also change the provider in **Admin settings** (`/beacon/admin-settings`): saving swaps the active provider immediately, without a restart. If the new configuration cannot be built, no provider is active until it is fixed; the previous one is never kept. :::note The provider is built from `Beacon:LLM` each time the process starts. Keep that section in step with what you choose in Admin settings, or save the Admin settings again after a restart. ::: | `Provider` | Required | Notes | |---|---|---| | `OpenAI` | `ApiKey`, `Model` | Calls the public OpenAI API. | | `Claude` (shown as **Anthropic** in the UI) | `ApiKey`, `Model` | Calls `https://api.anthropic.com`. | | `AzureOpenAI` | `Endpoint`, `ApiKey`, `Model` (the **deployment name**) | | | `Bedrock` | `Region`, `Model` (model id) | Needs the `Moberg.Beacon.AI.Bedrock` package and `AddBeaconBedrock()`; without them a stored Bedrock setting leaves no active provider. `BedrockAuthMode`: `IamRole` (default) → the default AWS credential chain (instance role, environment); `AccessKey` → `AwsAccessKeyId` and `AwsSecretAccessKey`; `TemporaryCredentials` → those plus `SessionToken`. A legacy `ApiKey` of `accessKey:secretKey` is used when both key fields are empty; under `IamRole` a warning is logged. | ```json { "Beacon": { "LLM": { "Provider": "Claude", "ApiKey": "", "Model": "", "FastModel": "", "Limits": { "MaxConcurrentRequests": 20 } } } } ``` ```json { "Beacon": { "LLM": { "Provider": "Bedrock", "Region": "eu-central-1", "Model": "" } } } ``` `Model` and `FastModel` are free text. Use whatever model ids your provider offers. `Model` does the work (SQL generation, documentation, actors); `FastModel` is stored with the configuration for a smaller model, but no feature uses it yet; every call uses `Model`. ### The request queue Every LLM call goes through one queue that allows at most `Limits:MaxConcurrentRequests` calls at a time (default 50) and makes up to three attempts for a retryable failure, with exponential backoff. The concurrency limit is read at startup. Token, request and budget figures (`TokensPerMinute`, `RequestsPerMinute`, `MonthlyBudget`) are recorded and shown in Admin settings; use your provider's own quotas and spend limits for hard caps. ## Local embeddings (optional) Semantic retrieval (finding the right tables, examples and documentation chunks for a question) can use a local ONNX embedding model. Nothing leaves your process. The model support is a separate package: `dotnet add package Moberg.Beacon.AI.Embeddings.Onnx`, then call `services.AddBeaconOnnxEmbeddings()` before or after `AddBeaconAI`. Without it, `AddBeaconAI` registers a disabled service; if `Enabled` is `true` it logs one warning the first time the embedding service is used (not at startup). ```json { "Beacon": { "Embeddings": { "Enabled": true, "ModelPath": "/models/bge-small-en-v1.5/model.onnx", "TokenizerPath": "/models/bge-small-en-v1.5/vocab.txt" } } } ``` - Model: **bge-small-en-v1.5** (384 dimensions, 512 tokens). Beacon does not ship the model files; download them and mount them read-only. - Embeddings are used only when `Enabled` is `true` **and** both files exist. Otherwise Beacon falls back to keyword retrieval. - On PostgreSQL vectors live in a `pgvector` column with an HNSW index; on SQL Server they are compared in memory. - Schedule the re-index jobs (`mcp-embedding-reindex`, `mcp-docchunk-reindex`); see [Background jobs](https://moberghr.github.io/beacon/integrate/background-jobs/#recurring-maintenance-jobs). ## What reaches the LLM provider By default Beacon sends **schema and documentation, not row values**: | Data | Sent? | |---|---| | Table and column names, types, relationships, documentation, glossary, verified examples | Yes (this is the grounding) | | The user's question | Yes | | Sample values from tables | Only with the per-project setting **sample-value collection** (off by default); never from PII or secret-like columns | | Values probed to ground a question (value grounding) | Only with **value grounding** on (off by default) | | Query results for an LLM judge in evaluations | Only with **eval judge** on (off by default) | | Database error text | Redacted: value-carrying errors are reduced to their class | Set the deployment lock `Beacon:Mcp:ForbidRowDataToLlm: true` to pin all three switches off for every project. Every LLM request that carries row-derived data is logged as event **7123 `RowDataSentToLlm`** with caller, project, data source ids and categories, never the values. ## Features that use AI | Feature | Needs | |---|---| | MCP `ask`, `get_query_context` | AI on; a provider for `ask` | | Project documentation | AI on, a provider, `EnqueueProjectDocumentation` in your scheduler | | Learning loop, evaluations | AI on; scheduled maintenance jobs; `EnqueueMcpEval` for eval runs | | AI actors | AI on, a provider, `EnqueueAiActorThinkCycle` in your scheduler | The MCP `query`, `dry_run`, `search`, `get_context` and `get_documentation` tools work without an LLM provider. More: [AI integration](https://moberghr.github.io/beacon/features/ai-integration/) · [Knowledge base & grounding](https://moberghr.github.io/beacon/features/knowledge-base/) · [AI actors](https://moberghr.github.io/beacon/features/ai-actors/). --- # MCP & AI clients > Expose Beacon's MCP server from your host, decide how AI clients authenticate (API keys or Entra ID callers), publish discovery metadata, and connect Claude Code, Claude, Cursor and VS Code. Source: https://moberghr.github.io/beacon/integrate/mcp/ Beacon's MCP server lets AI assistants explore and query your data through eight tools, always project-scoped, read-only, masked and audited. This page wires it into your host and connects clients. The tools themselves are described on [MCP server](https://moberghr.github.io/beacon/features/mcp-server/). ## Wiring ```csharp builder.Services.AddBeaconApiAuthorization(); // registers the Execute-scope policy MCP needs builder.Services.AddBeaconAI(builder.Configuration); builder.Services.AddBeaconMcp(); // … app.UseMcpResourceMetadataChallenge(); // before ApiKeyAuthMiddleware app.UseMiddleware(); app.UseBeaconJwtBearerAuthentication(); // only for Entra / JWT callers // … authentication, cookie, authorization … app.MapBeaconMcp(); // /beacon/mcp, Execute scope required app.MapMcpDiscovery(); // anonymous discovery documents ``` | Item | Value | |---|---| | Endpoint | `/beacon/mcp` | | Transport | Streamable HTTP | | Authentication | `Authorization: Bearer sk-sem_…` (API key) or `Authorization: Bearer ` | | Required scope | **Execute** (a Read-scoped key gets 403) | | Sessions | Clients that start with `initialize` (most clients today) get an `Mcp-Session-Id` (idle timeout 30 minutes) and need **session affinity** when you run several instances. Clients speaking the stateless protocol revision `2026-07-28` need no session. A request that is neither (no session and no `2026-07-28` metadata) is refused with 400. | `MapBeaconMcp()` maps the route with the Execute-scope policy, and two filters inside the MCP layer refuse any caller without Execute even if the route were mapped with a weaker policy. Do not map it yourself with `MapMcp(...)` and a bare `RequireAuthorization()`. :::note[Browser sessions] A signed-in browser session can authenticate to `/beacon/mcp` but reaches **no project** there. People try the tools in the UI instead: **MCP playground** (`/beacon/mcp-playground`). ::: ## Public URL and discovery Clients that connect by URL (claude.ai, Claude Desktop connectors, VS Code) discover how to authenticate from three anonymous documents: | Path | Content | |---|---| | `/.well-known/oauth-protected-resource` | RFC 9728 protected-resource metadata: `resource` (`{base}/beacon/mcp`), `scopes_supported`, `authorization_servers`, bearer in header only | | `/.well-known/oauth-protected-resource/beacon/mcp` | The same, at the path-inserted location | | `/.well-known/mcp/server-card.json` | Server card: name, transport, endpoint, authentication, the eight built-in tools | Every 401 from `/beacon/mcp` carries `WWW-Authenticate: Bearer resource_metadata="{base}/.well-known/oauth-protected-resource"`. **Set `Beacon:PublicBaseUrl`** (for example `https://beacon.example.com`) in every proxied or public deployment. Without it, the documents and challenges are built from the request's `Host` header, which the client controls; Beacon warns at startup. To point OAuth-capable clients at your identity provider: ```json { "Beacon": { "PublicBaseUrl": "https://beacon.example.com", "Mcp": { "ProtectedResource": { "AuthorizationServers": [ "https://login.microsoftonline.com//v2.0" ], "ScopesSupported": [ "api:///mcp.access" ] } } } } ``` | Key | Default | What it does | |---|---|---| | `AuthorizationServers` | None | Issuers published as `authorization_servers` (each must equal the server metadata's `issuer`). Takes precedence. | | `DeriveAuthorizationServerFromJwtIssuer` | `false` | Publish the bearer-JWT issuer instead (prefers an Entra `/v2.0` issuer). | | `ScopesSupported` | `["Execute"]` | Scopes published to clients. Set your identity provider's scope when you publish an external authorization server. | With none of these set and OIDC enabled, Beacon publishes its OIDC authority. ## Option A: API keys The simplest path: each person creates a key for themselves. 1. In **API keys** (`/beacon/api-keys`), create a key with the **Execute** scope, limited to the projects the assistant should use, with a short expiry. (Scripts can do the same through `POST /beacon/api/api-keys`; see [headless first run](https://moberghr.github.io/beacon/integrate/authentication/#headless-first-run-scripts-and-ci).) 2. Copy it. It is shown once. 3. Configure the client with `Authorization: Bearer sk-sem_…` (below). The key acts as its owner, only in the key's projects, capped by the owner's current role: reading tools and saved-query tools work for any owner, while `query` and `ask` need an owner who can query (Editor or above). When a key reaches several projects, the assistant passes `project_id` on each call. ## Option B: Entra ID callers For organisation-wide access without personal keys, Beacon accepts Entra access tokens on `/beacon/mcp` and maps each token to an **MCP caller** with projects and a scope. Two kinds: - **Users**: delegated (on-behalf-of) tokens from a client app your users sign in to; optionally auto-provisioned as Beacon users. - **Systems**: app-only tokens of a known application or service principal. ```csharp builder.Services.AddBeaconJwtAuthentication(jwt => { jwt.EnableBearerAuthentication = true; jwt.Validation.JwksEndpoint = "https://login.microsoftonline.com//discovery/v2.0/keys"; jwt.Validation.ValidIssuers = [ "https://login.microsoftonline.com//v2.0" ]; jwt.Validation.ValidAudiences = [ "api://" ]; }); ``` ```json { "Beacon": { "Mcp": { "Callers": { "AllowedTenants": [ "" ], "Users": { "Enabled": true, "RequiredGroups": [ "" ], "ProjectIds": [ 4 ], "GroupProjects": { "": [ 7 ] }, "DefaultRoleName": "Viewer" }, "Systems": [ { "Name": "reporting-bot", "ClientId": "", "ProjectIds": [ 4 ], "Scope": "Execute" } ] } } } } ``` | Key | Default | What it does | |---|---|---| | `AllowedTenants` | Required | Tenants whose tokens may map to a caller. Required once any caller is configured. | | `Users:Enabled` | `false` | Accept delegated user tokens. | | `Users:RequiredRoles` / `RequiredGroups` | None | The user needs one of them; with neither set, `AllowAnyTenantUser` must be `true`. | | `Users:ProjectIds`, `Users:GroupProjects` | None | Projects every accepted user gets, plus extra projects per group or app role. | | `Users:Scope` | `Execute` | `Read` or `Execute`. | | `Users:AutoProvision` / `DefaultRoleName` | `true` / `Viewer` | Create a Beacon user on first call, with this role (never Admin or a functional role). | | `Systems[]` | None | `Name`, exactly one of `ClientId` (app-only token) or `ObjectId` (service principal), `ProjectIds`, `Scope`, `HostClaims`. | An absent `Callers` section admits no JWT caller on MCP; it fails closed. Startup validates the section, including that bearer JWT validation is on with explicit issuers and audiences. Audit rows identify Entra callers by a keyed hash (`Beacon:Mcp:CallerHashKey`, else derived from the encryption key). Full reference: [Entra ID callers](https://moberghr.github.io/beacon/features/mcp-entra-callers/). ## Connect a client ### Claude Code ```bash claude mcp add --transport http beacon https://beacon.example.com/beacon/mcp \ --header "Authorization: Bearer $BEACON_MCP_API_KEY" ``` Or share it with your team in the repository's `.mcp.json` (Claude Code expands environment variables): ```json { "mcpServers": { "beacon": { "type": "http", "url": "https://beacon.example.com/beacon/mcp", "headers": { "Authorization": "Bearer ${BEACON_MCP_API_KEY}" } } } } ``` ### Cursor `~/.cursor/mcp.json` or `.cursor/mcp.json` in the project: ```json { "mcpServers": { "beacon": { "url": "https://beacon.example.com/beacon/mcp", "headers": { "Authorization": "Bearer sk-sem_…" } } } } ``` ### VS Code `.vscode/mcp.json`: ```json { "inputs": [ { "type": "promptString", "id": "beacon-key", "description": "Beacon API key", "password": true } ], "servers": { "beacon": { "type": "http", "url": "https://beacon.example.com/beacon/mcp", "headers": { "Authorization": "Bearer ${input:beacon-key}" } } } } ``` ### Claude (claude.ai and Claude Desktop) Add Beacon as a **custom connector** with the URL `https://beacon.example.com/beacon/mcp`. Connectors authenticate with OAuth, so this path needs Option B: publish your Entra authorization server and scope through `Beacon:Mcp:ProtectedResource`, register the client's redirect URI in your Entra app, and map users with `Beacon:Mcp:Callers:Users`. The step-by-step Entra setup is on [Entra ID callers](https://moberghr.github.io/beacon/features/mcp-entra-callers/). Client configuration formats change; if a snippet does not work, check the client's own documentation. The endpoint, header and transport above are what Beacon needs. ## Check it works ```bash BEACON=https://beacon.example.com curl -s $BEACON/.well-known/mcp/server-card.json | jq '.tools[].name' H=(-H "Authorization: Bearer $BEACON_MCP_API_KEY" -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream") # 1. initialize: the response header Mcp-Session-Id names the session SESSION=$(curl -s -D - -o /dev/null "${H[@]}" $BEACON/beacon/mcp \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}' \ | awk -F': ' 'tolower($1)=="mcp-session-id"{print $2}' | tr -d '\r') # 2. confirm initialisation curl -s "${H[@]}" -H "Mcp-Session-Id: $SESSION" -H "MCP-Protocol-Version: 2025-06-18" $BEACON/beacon/mcp \ -d '{"jsonrpc":"2.0","method":"notifications/initialized"}' # 3. list the tools curl -s "${H[@]}" -H "Mcp-Session-Id: $SESSION" -H "MCP-Protocol-Version: 2025-06-18" $BEACON/beacon/mcp \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' ``` A client on the stateless revision skips the session: it sends `MCP-Protocol-Version: 2026-07-28` and `Mcp-Method: tools/list` headers (plus `Mcp-Name: ` on `tools/call`) and puts `{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}` in `params._meta`. Then ask the assistant something like *"Use Beacon's get_context to describe my project."* | Symptom | Cause | |---|---| | 400 "A new session can only be created by an initialize request" | The client sent a request without a session: start with `initialize` (or use the stateless `2026-07-28` revision). | | 401 with `resource_metadata` | No or invalid credential. | | 403 at the door | The key has the Read scope, or the JWT caller is not mapped. | | "Permission denied: this credential does not have the Execute scope" | Same, reported as a tool error. | | "No projects are available to you." | The key or caller reaches no project, or a browser session is calling. | | "Access denied: you may read project N but not run ad-hoc queries in it." | Viewer-level access: `query`/`ask` need Query (Editor). | ## Add your own tools - Publish an approved saved query as an MCP tool `q_` without code: [Saved query tools](https://moberghr.github.io/beacon/features/saved-query-tools/). - Expose endpoints of your own API as `api_` tools: [Extending Beacon](https://moberghr.github.io/beacon/integrate/extending/#your-endpoints-as-mcp-tools). ## Governing MCP Per-project MCP settings (row limits, read-only, PII detection, content retention, learning) live on **MCP settings** (`/beacon/mcp-settings`), editable by a SecurityAdmin and readable by an Auditor. Deployment locks (`Beacon:Mcp:ForceReadOnly`, `ForcePiiDetection`, `ForceNoContentRetention`, `ForbidRowDataToLlm`) and ceilings (`Beacon:Mcp:Ceilings`) bind them from configuration. Every call is audited; see [MCP observability & audit](https://moberghr.github.io/beacon/features/mcp-observability-and-audit/). --- # Extending Beacon > Make your own application part of Beacon: expose your EF Core DbContext as a read-only data source, import your Markdown docs as grounding, turn your HTTP endpoints into MCP tools, and replace Beacon's extension points. Source: https://moberghr.github.io/beacon/integrate/extending/ An embedded Beacon can know your application, not only your databases. Three host extensions, all opt-in and all scoped to one **host project**: | Extension | You get | Page | |---|---|---| | `ExposeDbContext` | Your EF Core model as a read-only data source, with a table allow-list and column masking | [Host DbContext](https://moberghr.github.io/beacon/features/host-dbcontext/) | | `ExposeDocs` | Your Markdown documentation imported as grounding for `ask`, `search` and `get_documentation` | [Host docs](https://moberghr.github.io/beacon/features/host-docs/) | | `AddHostEndpointTools` | Your HTTP endpoints as MCP tools `api_`, dispatched in-process through your own authorization | [Host endpoint tools](https://moberghr.github.io/beacon/features/host-endpoint-tools/) | ## Your DbContext as a data source ```csharp builder.Services.AddBeaconServices(builder.Configuration, options => { /* … */ }) .ExposeDbContext(x => { x.ProjectName = "Shop"; // the host project; defaults to the data-source name x.ReadOnlyConnection("ShopReadOnly") // ConnectionStrings:ShopReadOnly, a read-only login .AllowTables("Orders", "OrderLines", "Customers") .AllowEntity() .ExcludeColumns(y => y.Table == "Customers" && y.Name == "Notes") .MaskColumns(y => y.Name is "Email" or "Phone"); }) .AddSqlServerConnector() .UseSqlServer(); // ends the chain: ExposeDbContext comes first ``` - Tables are **default-deny**; unknown names fail startup. - Columns whose names look like secrets (`password`, `token`, `apikey`, `hash`, `otp`, …) are always excluded unless you lift one explicitly with `IncludeSecretLikeColumn("Table.Column")`. - The connection string is **never stored**. Beacon keeps only its name and reads it from configuration when it connects. - SQL Server and PostgreSQL models are supported. - Every statement is checked against the exposed slice in the execution gate and again in the provider; masked columns are masked by output position. ## Your docs as grounding ```csharp .ExposeDocs(x => { x.ProjectName = "Shop"; x.FromDirectory("docs", "**/*.md").Exclude("drafts/**"); // x.FromEmbeddedResources(typeof(Program).Assembly, "Shop.Docs."); // x.MaxDocumentBytes = 512 * 1024; }) ``` `.md`, `.markdown` and `.txt` files are imported into the project, chunked and embedded. MCP clients read them through `search` (`[IMPORTED_DOC]` results), `get_documentation document=…` and `get_context format=agents_md`. ## Sync at startup Both extensions need one startup call, **after** Beacon's schema is migrated: ```csharp var app = builder.Build(); Beacon.Core.ServiceConfiguration.UseBeacon(app.Services, createSchema: true); await app.Services.SyncBeaconHostAsync(); // data sources first, then docs; idempotent; replica-safe lock ``` `SyncBeaconHostDataSourcesAsync()` syncs the data sources only. ## Your endpoints as MCP tools ```csharp builder.Services.AddBeaconMcp() .AddHostEndpointTools(x => { x.ProjectName = "Shop"; // the host project the tools belong to // x.AllowWriteTools = true; // off by default }); app.MapGet("/api/orders/{id:int}", (int id, OrderService orders) => orders.Find(id)) .RequireAuthorization("ViewOrders") .WithBeaconTool("order_by_id", "One order by id.", readOnly: true); ``` Or on a controller action: `[BeaconTool("customer_orders", Description = "…", ReadOnly = true)]`. - `ReadOnly` must be declared explicitly; Beacon never infers it from the HTTP verb. - The endpoint must carry authorization metadata (or your host has a fallback policy); otherwise startup refuses it. - Calls run in-process through your endpoint's own pipeline, as a host principal built for the MCP caller (`IMcpHostPrincipalFactory`). - Only mapped Entra callers (users or systems) of the host project can call them; API-key callers cannot. - Write tools (`ReadOnly = false`) need `AllowWriteTools`, are offered to **user** callers only and are audited as `api_:write`. - Above 20 tools (`NamedToolLimit`), clients see `search_api` + `call_api` instead of one tool per endpoint. ## Saved queries as tools without code A SecurityAdmin can publish an approved saved query as an MCP tool `q_` from the query's page. See [Saved query tools](https://moberghr.github.io/beacon/features/saved-query-tools/). ## Replaceable services Beacon registers most services with `TryAdd`, so **your registration wins if you register first** (before `AddBeaconServices`, `AddBeaconApiServices` or `AddHostEndpointTools`, depending on who registers it). | Interface | Default | Replace it to | |---|---|---| | `IBeaconScheduler` | None (required) | Use your job runner. | | `IEmailAdapter` | none | Send email (`AddEmailAdapter()`). | | `IBeaconAuthenticationProvider` | user store | Check passwords somewhere else. | | `IBeaconAuthorizationProvider` | user store / allow-all | Cap users' global read/write from your own model. | | `IProjectMembershipSource` | Beacon's membership table | Take project membership from your application. Beacon's membership endpoints then answer 409. | | `IMcpCallerMapper` | `Beacon:Mcp:Callers` | Map JWT callers to projects and scopes your own way. | | `IMcpHostPrincipalFactory` | copies name/email/oid; system callers get `HostClaims` | Build the principal your endpoints expect for MCP calls. | | `IHostEndpointDispatchMiddleware` | none | Run your own middleware logic (tenant, culture, audit) around dispatched tool calls. Ships in `Moberg.Beacon.MCP` (namespace `Beacon.Core.HostEndpoints`). | | `IApprovalNotifier` | SignalR | Notify approvers elsewhere. | | `IBeaconUserContext` | `HttpContextUserContext` (Beacon.Api) | Resolve the current user differently. Replace it in both places it is registered. | | `IBeaconRequestPrincipal` | Core: no request; Beacon.Api and Beacon.MCP: the current `HttpContext` | Tell Beacon's core who the request's caller is from your own request model. Call `UseRequestPrincipal()` before or after Beacon's registrations: it replaces Beacon's own principals (Core's default and the `HttpContext` one) in any order, and Beacon's web registrations after it keep yours. A principal registered with `AddSingleton` before Beacon's registrations stays, and after them wins as the last registration. A `TryAdd*` after `AddBeaconServices` is a no-op, because Core has already registered its default. A host that maps Beacon's endpoints over Core's default fails to start. | `IProjectAccessEvaluator` is replaceable in principle but it is the single decider for project access; replace it only with great care, and keep the project-authorization tests green if you fork it. --- # Production checklist > What to set before Beacon faces real users and real data: secrets, Data Protection, reverse proxy, sign-in, locks, audit, MCP, scaling, observability and upgrades. Source: https://moberghr.github.io/beacon/integrate/production/ Go through this list before the first real user signs in. Each line links to where it is explained. ## Secrets and keys - [ ] `Beacon:EncryptionKey` comes from a secret store or environment variable, never a file in source control; its backup is stored separately from database backups. → [Configuration](https://moberghr.github.io/beacon/getting-started/configuration/) - [ ] You know how to **rotate** it: move the old key to `Beacon:PreviousEncryptionKeys:0`, set the new one, set `Beacon:Mcp:CallerHashKey` to the old key first if you want MCP caller hashes to stay comparable, run the re-encryption job, then remove the old key. Set `Beacon:Audit:IntegrityKey` before the first rotation: while it is unset, security audit events are signed under a key derived from the caller hash key or the encryption key, and verify as *unknown key* once that secret leaves the configuration. → [Keys and rotation](https://moberghr.github.io/beacon/features/security-audit/#keys-and-rotation) - [ ] `Beacon:DataProtection:ApplicationName` is set (one value per environment), and the key ring is **encrypted at rest** (`ProtectKeysWithCertificate` / `ProtectKeysWithAzureKeyVault`), otherwise the host does not start outside `Development`. → [Host wiring](https://moberghr.github.io/beacon/integrate/host-setup/#data-protection) - [ ] OIDC client secrets, LLM API keys and SMTP passwords come from the secret store. - [ ] `Beacon:UserManagement:SetupToken` is set for multi-instance first runs, and removed afterwards. ## Reverse proxy - [ ] TLS terminates at the proxy, and the host runs `UseForwardedHeaders` with **only your proxies** listed, first in the pipeline. The login throttle (10 attempts / 60 s per IP) and the cookie's Secure flag depend on it. ```csharp var forwarded = new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto }; forwarded.KnownProxies.Clear(); forwarded.KnownNetworks.Clear(); forwarded.KnownProxies.Add(IPAddress.Parse("10.0.0.5")); app.UseForwardedHeaders(forwarded); ``` - [ ] `Beacon:PublicBaseUrl` is the public origin (e.g. `https://beacon.example.com`), so MCP discovery documents do not echo the client's `Host` header. → [MCP & AI clients](https://moberghr.github.io/beacon/integrate/mcp/#public-url-and-discovery) - [ ] `options.BaseUrl` (or **Admin settings → Base URL**) is the public UI URL **including `/beacon`**, so notification links work. - [ ] HTTPS redirection applies to `/beacon/mcp` too, because API keys travel in headers. - [ ] Proxy and Kestrel timeouts allow long requests (AI documentation, `ask`): the reference host uses 5 minutes. ## Sign-in and access - [ ] Authorization is on (`options.Authorization.Enabled = true`). - [ ] SSO admission is narrow: `AllowedTenants`, `BlockGuests` (default on), `RequiredGroups` or `RequiredRoles`; `RequireMfa` if your provider issues `amr`/`acr`. → [Sign-in & identity](https://moberghr.github.io/beacon/integrate/authentication/#oidc--sso) - [ ] Local passwords off once SSO works, with one break-glass account in a vault; alert on event 7119 `BreakGlassSignIn`. - [ ] `Beacon:Authorization:RequireProjectMembership: true`; projects planned; data sources linked only where needed. → [Access control](https://moberghr.github.io/beacon/integrate/access-control/) - [ ] Few Admins; duties given as functional permissions; `SeparationOfDuties` on if your auditors expect it. - [ ] Approval workflow on with at least two Approvers, `RequireApprovedVersionToRun` for regulated data. → [Approvals](https://moberghr.github.io/beacon/features/approvals/) - [ ] API keys: decide `Beacon:ApiKeys:MaxLifetimeDays` and `MaxOwnerInactivityDays`. ## Data and SQL - [ ] Every data source uses a **read-only login**, including PostgreSQL and MySQL. → [Connectors](https://moberghr.github.io/beacon/integrate/connectors/#creating-read-only-logins) - [ ] Deployment locks reviewed: `Beacon:Mcp:ForceReadOnly` and `ForcePiiDetection` stay `true`; consider `ForbidRowDataToLlm` and `ForceNoContentRetention`. → [Access control](https://moberghr.github.io/beacon/integrate/access-control/#5-deployment-locks) - [ ] `Beacon:Query:MaxExecutionRows` (default 100,000) and `StatementTimeoutSeconds` (default 120) fit your workloads. - [ ] `Beacon:Mcp:Ceilings:MaxRowLimit` set if project admins must not raise the MCP row limit. - [ ] `Beacon:Migrations:Enabled` stays `false` unless you need data migration. It is the only write path. - [ ] Notification destinations restricted as needed: `Beacon:Notifications:AllowedHosts:*`, `DisabledTypes`; private networks stay unreachable unless listed in `AllowedPrivateNetworks`. → [Notifications](https://moberghr.github.io/beacon/features/notifications/) ## Audit and observability - [ ] `Beacon:Mcp:Audit:RetentionDays` set to your retention policy and the purge job scheduled; `Audit:Required: true` if a tool result must never be returned without its audit row. → [MCP observability & audit](https://moberghr.github.io/beacon/features/mcp-observability-and-audit/) - [ ] Security log events (structured log events 7101 to 7123: membership, roles, sessions, break-glass, access refused, row data to LLM) are shipped to your log pipeline and alerted on. The log events themselves are not stored in Beacon's database; the actions most of them report are also recorded in the security audit trail. → [Security model](https://moberghr.github.io/beacon/features/security-model/#12-audit-and-security-events) - [ ] Security audit trail: `Beacon:Audit:IntegrityKey` set from a secret store (before you change the encryption key or the caller hash key), the verification job (`ISecurityAuditVerificationService.VerifyNewEventsAsync`, hourly) and the retention purge (`ISecurityAuditRetentionService.PurgeExpiredAsync`) scheduled, `Beacon:Audit:RetentionDays` set to your retention policy (at least 365; events are kept forever while it is unset), the `Beacon.Audit` events 9104 to 9110 shipped to your SIEM, and alerts on any increment of `beacon.security_audit.write_failures` and `beacon.security_audit.verification_failures`. → [Security audit trail](https://moberghr.github.io/beacon/features/security-audit/) - [ ] To make the trail append-only at the database as well, Beacon's connection uses a role that cannot update or delete `security_audit_events` rows other than through the purge, or triggers do the same. → [What the trail does and does not prove](https://moberghr.github.io/beacon/features/security-audit/#what-the-trail-does-and-does-not-prove) - [ ] Where MCP audit content is retained, the SQL of every statement a call ran is stored with it: reviewed against your privacy policy; `Beacon:Mcp:Audit:RequireContent` pins retention on if the evidence must be kept. → [MCP observability & audit](https://moberghr.github.io/beacon/features/mcp-observability-and-audit/#keeping-audit-content-the-requirecontent-lock) - [ ] Structured JSON console logs (`Logging:Console:FormatterName=json`, `IncludeScopes=true`) if your platform correlates by trace id. - [ ] OpenTelemetry: add `AddBeaconInstrumentation()` to your tracer and meter providers; you choose the exporter. Keep notification HTTP calls out of traces (their URLs carry secrets). `NotificationRequests.IsNotificationRequest` identifies them. - [ ] Health check `GET /beacon/api/health` (anonymous, checks the database within 5 s) wired to your load balancer. On a brand-new deployment with user management on, it redirects to `/beacon/setup` until the first administrator exists. Complete setup before the probe goes live. ## Scaling - [ ] Several instances: shared Data Protection key ring (Beacon's database does this), a SignalR backplane (`ConfigureSignalR`), **session affinity for `/beacon/mcp`** (legacy MCP clients hold an `Mcp-Session-Id` in memory), and a job runner that fires each recurring job once. - [ ] Migrations run once per deployment, not from every replica at the same time. → [Metadata database](https://moberghr.github.io/beacon/integrate/database/#creating-and-migrating-the-schema) - [ ] Database pool sized for job workers plus web traffic. ## Upgrades - [ ] All `Moberg.Beacon.*` packages on the same version. - [ ] Read the release notes of every version you upgrade to: a release can change defaults, and the notes say what to do. - [ ] Back up the metadata database before migrating; migrations are append-only and run forward. - [ ] After upgrading every instance, run one-off jobs the release notes name (for example recipient-secret encryption). --- # Troubleshooting > Startup failures, empty pages, 401s and 404s, jobs that do nothing, MCP clients that cannot connect: symptoms, causes and fixes for an embedded Beacon. Source: https://moberghr.github.io/beacon/integrate/troubleshooting/ ## The host does not start | Message (or symptom) | Cause | Fix | |---|---|---| | `Implementation of IBeaconScheduler is required.` | No scheduler registered. | `options.AddBeaconScheduler()`. → [Background jobs](https://moberghr.github.io/beacon/integrate/background-jobs/) | | `CS9137` (interceptors) at build | `AddOpenApi`/`MapOpenApi` without a direct `Microsoft.AspNetCore.OpenApi` reference. | Add the package to your project, or drop the two calls. | | `CS0122 'RoleService' is inaccessible` | A claims transformation copied from the reference host uses Beacon's internal role constants. | Use the version on [Host wiring](https://moberghr.github.io/beacon/integrate/host-setup/#the-claims-transformation). | | An `InvalidOperationException` about `Beacon:EncryptionKey` | Key missing, too short, not random, a published example, or a `${…}` placeholder. | `openssl rand -base64 32` into a secret store / `Beacon__EncryptionKey`. | | `Connection string 'BeaconContext' is not configured` | `UsePostgreSql()`/`UseSqlServer()` without `ConnectionStrings:BeaconContext`. | Set it, or pass a connection string. In development check that user-secrets load (they only load in `Development`). | | An error about `Beacon:Schema` | The key is set to something other than `beacon`. | Remove it. The schema is fixed. | | `Beacon:DataProtection:ApplicationName must be set outside Development` | `AddBeaconDataProtection` outside `Development` without an application name. | Set it, or run with `ASPNETCORE_ENVIRONMENT=Development` locally. | | Data Protection keys "persisted without encryption at rest" | Outside `Development` the key ring has no protector. | Chain `ProtectKeysWithCertificate(...)` / `ProtectKeysWithAzureKeyVault(...)`, or set `Beacon:DataProtection:AllowUnencryptedKeys` deliberately. | | `Beacon:UserManagement:SetupToken must be at least 32 characters` | Short setup token. | Use a longer one or unset it. | | Local login off and "nobody could sign in" | `LocalLogin:Enabled=false` without SSO, external login or a break-glass name. | Enable OIDC, configure `ExternalLogin`, or add a break-glass name. | | OIDC: missing Authority/ClientId/ClientSecret or admission rule | `Oidc:Enabled` without required values or without `AllowedTenants` (or `AllowAnyTenant`). | Complete the section. | | `Beacon:Mcp:Callers` validation errors | Callers configured without `AllowedTenants`, without bearer JWT validation, or with both `ClientId` and `ObjectId`. | See [MCP & AI clients](https://moberghr.github.io/beacon/integrate/mcp/#option-b-entra-id-callers). | | `Beacon host endpoint tools are misconfigured:` | A `[BeaconTool]` endpoint without `ReadOnly`, without authorization metadata, with an untrusted auth scheme, or a write tool without `AllowWriteTools`. | Fix each listed endpoint. | | `AddHostEndpointTools can only be called once.` | Called twice. | Call it once, after `AddBeaconMcp()`. | | Options validation for `Beacon:Query`, `Beacon:Mcp`, `Beacon:ApiKeys`, `Beacon:Notifications`, `Beacon:Authorization`… | A value is out of range or malformed. | The message names the key; see [Configuration](https://moberghr.github.io/beacon/getting-started/configuration/). | ## Database | Symptom | Cause | Fix | |---|---|---| | `3D000: database "…" does not exist` | The database itself is missing; `UseBeacon` creates the schema, not the database. | `CREATE DATABASE beacon;` (or `POSTGRES_DB=beacon` on the container). | | `relation "…" does not exist` / `Invalid object name 'beacon.…'` | Beacon's schema was never migrated. | Call `Beacon.Core.ServiceConfiguration.UseBeacon(app.Services, createSchema: true)` at startup. → [Metadata database](https://moberghr.github.io/beacon/integrate/database/) | | `extension "vector" is not available` | pgvector is not installed for this PostgreSQL server. | Install pgvector (Azure: allow `VECTOR` in `azure.extensions`), or use an image that has it (e.g. `pgvector/pgvector`). | | `permission denied to create extension "vector"` | The migrating login cannot create extensions. | Create it once as an administrator: `CREATE EXTENSION vector;`. | | `type "vector" does not exist` with your own `NpgsqlDataSource` | The extension lives in a schema that is not on the search path. | Install it in `public` (Beacon's search path is `beacon, public`). | ## The UI | Symptom | Cause | Fix | |---|---|---| | 404 at `/` | The UI lives at `/beacon`. | Open `/beacon`, or redirect `/` to it yourself. | | 404 for `/beacon` or blank page; asset 404s | `MapBeaconUi()` or `UseStaticFiles()` missing; or, building from source, the React app was not built (`Debug` builds skip it). | Add both calls; build `Release`, or `-p:BuildReactInDebug=true`. | | Endless redirect to `/login` (not `/beacon/login`) | `AddBeaconCookieAuthentication("/")`. | Pass `"/beacon"`. | | Always redirected to `/beacon/setup`, even `/beacon/api/health` | User management on and no administrator yet. | Complete first-run setup with the setup token (printed once to the console, or `Beacon:UserManagement:SetupToken`). | | Signed in, but every API call answers 401 | Middleware order: `BeaconCookieAuthMiddleware` must run before `UseAuthorization`. | Follow the [pipeline order](https://moberghr.github.io/beacon/integrate/host-setup/#the-middleware-pipeline). | | Saves fail with 400 "antiforgery" | No `AddAntiforgery`/`UseAntiforgery`, or a different header name without the SPA header. | Configure antiforgery with `HeaderName = "X-XSRF-TOKEN"`. | | An error shows as "Internal server error" in the UI | Business-rule exceptions are not mapped to problem+json. | Add the [error handler](https://moberghr.github.io/beacon/integrate/host-setup/#error-responses). | | A project, query or data source is missing for a user | Project access: the project is restricted and they are not a member, or the data source is not linked to a project they can read. | Add them as a member or link the data source. Restricted resources answer 404 on purpose. | | No live updates | Realtime off, or `AddBeaconApiServices()` missing, or a proxy blocks WebSockets on `/beacon/api/hub`. | Check `realtimeEnabled` in `GET /beacon/api/auth/me`. | ## Scheduled checks | Symptom | Cause | Fix | |---|---|---| | Subscriptions never run | Your scheduler does not register the recurring job, or no worker is running. | Check `AddOrUpdate` and your runner's dashboard. | | Jobs run but do nothing; log warns about a scope without project access | The job's scope was not marked as system work. | Call `ISystemWorkScope.MarkAsSystemWork()` first in every job. | | Run recorded as **Skipped** | The subscription's owner can no longer run it (removed from the project, role lowered, disabled). | Give it a new owner (UserAdmin or Admin) or restore access. | | Run refused: "not approved" | Approval workflow on and the query's current version is not approved. | An Approver approves the change. | | `NotImplementedException: … not implemented in this host` | The feature calls a scheduler member you left at its default. | Implement `AddOrUpdateDataQualityJob`, `EnqueueProjectDocumentation`, `EnqueueAiActorThinkCycle` or `EnqueueMcpEval`. | | Notification links 404 | `BaseUrl` lacks `/beacon`. | Set it to `https://host/beacon`. | | A recipient receives nothing | Destination not allowed by `Beacon:Notifications`, or awaiting approval. | Check the startup policy log line and **Approvals**. | ## MCP | Symptom | Cause | Fix | |---|---|---| | 401 | No or invalid key/token. | Send `Authorization: Bearer sk-sem_…`. | | 403 | Read-scoped key, or an unmapped JWT caller. | Use an Execute key, or map the caller. | | "No projects are available to you." | The key names no reachable project, or it is a browser session. | Create the key with a project; browsers use the playground. | | "Project access could not be determined; retry the call." | A transient failure while resolving access. | Retry; check the database. | | Discovery documents show the wrong host | `Beacon:PublicBaseUrl` unset behind a proxy. | Set it. | | Calls fail randomly across instances | Legacy MCP sessions without affinity. | Enable session affinity for `/beacon/mcp`. | Still stuck? Compare your `Program.cs` with `src/Beacon.SampleProject/Program.cs`, and [open an issue](https://github.com/moberghr/beacon/issues).