Single Application (standard flow)
The canonical SqlOS setup: describe one application in one AddSqlOS call and let SqlOS derive the protocol consequences.
This is the standard SqlOS shape and the one almost every product should start with: one SaaS app, described once. You get organizations, users, sessions, SSO, invitations, and permissions with a derived first-party OAuth client, surface audiences, and metadata documents. The multiple-applications shape exists for when a second application must sign in with your accounts.
builder.AddSqlOS<AppDbContext>(options =>
{
options.UseSingleApplication("Todo", app =>
{
app.Origin = "https://todo.example.com";
});
});This expands into the same seeded client records SqlOS already uses. It does not create a separate runtime path.
The same description can carry the application's protected surfaces, branding, and authorization model. SqlOS derives the protocol consequences (audiences, RFC 9728 documents, CIMD when Mcp is set). The host maps Microsoft's MCP SDK and locks routes itself:
builder.AddSqlOS<AppDbContext>(
db => db.UseSqlServer(connectionString),
options => options.UseSingleApplication("Todo", app =>
{
app.Origin = "https://todo.example.com";
app.Api = "/api"; // resource id {Origin}/api
app.Mcp = "/mcp"; // resource id {Origin}/mcp
app.Brand(page => page.PrimaryColor = "#0f172a"); // AuthPage branding seed
// app.Headless("/auth/authorize"); // your own sign-in UI instead of the hosted pages
app.Authorization(fga => fga // FGA model seed
.ResourceType("todo", "Todo")
.Permission("TODO_READ", "Read todos", "todo")
.Role("todo_owner", "Owner").Can("TODO_READ"));
}));
var app = builder.Build();
app.MapGet("/api/todos", ...).RequireAuthorization();
app.MapMcp("/mcp").RequireAuthorization("SqlOS.Mcp");
app.Run();| Declaration | What SqlOS does |
|---|---|
Api = "/api" | Resource id {Origin}/api: default SqlOS JWT scheme audience, first-party client audience, and /.well-known/oauth-protected-resource. Lock routes with RequireAuthorization(). |
Mcp = "/mcp" | Resource id {Origin}/mcp, /.well-known/oauth-protected-resource/mcp, CIMD + resource indicators, and scheme/policy SqlOS.Mcp. Map Microsoft's MCP SDK and call RequireAuthorization("SqlOS.Mcp"). See MCP server. |
Brand(...) | Forwards to AuthServer.SeedAuthPage on top of the single-application defaults. |
Headless("/auth/authorize") | Forwards to AuthServer.UseHeadlessAuthPage with a generated BuildUiUrl that sends browser interaction to {Origin}/auth/authorize with the standard headless parameters. See Hosted or headless. |
Authorization(...) | Forwards to Fga.Seed. |
The auth server, hosted sign-in pages, and dashboard are mapped by AddSqlOS at startup.
AddSqlOS registers the SqlOS JWT scheme with the Api audience when app.Api is set, and scheme/policy SqlOS.Mcp when app.Mcp is set. Audience lives on the scheme, the same way JwtBearerOptions.Audience does. Call RequireAuthorization() on the groups you lock. A missing or wrong-audience token is answered with 401 and a Bearer challenge. Handlers read the result through HttpContext.GetSqlOSValidatedToken() and HttpContext.User. SqlOS does not wrap routes from a path string and does not handle CORS.
The derived client is a public PKCE client for whatever front end you build: a browser SPA, a native app, or an agent. It completes the authorization-code flow with its standard OIDC library, requests resource={Origin}/api, and sends the token as a bearer. Add a native app's custom-scheme or loopback callback to RedirectUris, and set AllowNativeHeadlessAuth = true if it drives the headless authentication API directly. The Notes sample is a complete bearer-only host with API and MCP.
The hosted SqlOS pages are the default and need nothing. To draw every sign-in screen yourself, add one line to the same description:
options.UseSingleApplication("Todo", app =>
{
app.Origin = "https://todo.example.com";
app.Headless("/auth/authorize"); // your page at {Origin}/auth/authorize
});SqlOS then redirects /sqlos/auth/authorize to https://todo.example.com/auth/authorize?request=…&view=… with error, email, displayName, pendingToken, mfaToken, consentToken, and ui_context when present — the parameters the @sqlos/headless package reads. Brand(...) still applies; the headless view model exposes the branding. When the UI lives on another origin or you need a different query shape, take full control:
app.Headless(headless =>
{
headless.HeadlessApiBasePath = "/sqlos/auth/headless"; // default
headless.BuildUiUrl = context => $"https://ui.example.com/login?request={context.RequestId}&view={context.View}";
});BuildUiUrl present means headless; there is no second switch. Continue with Build your own login and signup UI and the headless reference.
Surface paths are validated at startup: they must be absolute, non-root, distinct and non-nested, and must not overlap /.well-known, the auth base path, or DashboardBasePath. Api and Mcp are separate audiences, so a token minted for /api is rejected at /mcp and vice versa. Inspect GetSqlOSValidatedToken()?.Scope in the handler when a client's granted ceiling must be checked.
| Setting | Default |
|---|---|
| Application name | The provided name |
| Client ID | Stable slug from the name, such as todo |
| Audience | {Origin}{Api} when Api is set; otherwise the client ID |
| Redirect URI | {Origin}/auth/callback |
| Client type | public_pkce |
| PKCE | Required, S256 only |
| First-party | true |
| Scopes | openid, profile, email, offline_access |
| Auth page title | Sign in to {ApplicationName} |
| Email application name | {ApplicationName} |
| Application access | all_organizations |
Single-application mode keeps DCR off, and keeps CIMD and resource indicators off unless you declare an Mcp surface (which needs both for portable MCP clients) or opt in explicitly.
builder.AddSqlOS<AppDbContext>(options =>
{
options.UseSingleApplication("Todo", app =>
{
app.Origin = "https://todo.example.com";
app.ClientId = "todo-web";
app.Audience = "https://todo.example.com/api";
app.RedirectPath = "/auth/callback";
app.AllowedScopes = ["openid", "profile", "email", "offline_access", "todos.read", "todos.write"];
app.EnablePasswordSignup = true;
app.EnabledCredentialTypes = ["password"];
});
});Origin must be an absolute http or https origin without a query string or fragment. RedirectPath must start with /. RedirectUris adds callbacks next to {Origin}{RedirectPath}; any absolute URI without a fragment is accepted, including com.example.todo:/callback for a native app. AllowNativeHeadlessAuth lets a native app use the headless authentication API with this client.
Deployment-friendly projects can bind from configuration:
{
"SqlOS": {
"Application": {
"Name": "Todo",
"Origin": "https://todo.example.com",
"ClientId": "todo-web",
"Api": "/api",
"Mcp": "/mcp",
"RedirectPath": "/auth/callback"
}
}
}builder.AddSqlOS<AppDbContext>(options =>
{
options.UseSingleApplication(builder.Configuration);
});Api and Mcp bind the same way as in code. Hosting the MCP server itself is the host's AddMcpServer / MapMcp calls, since tool registration is application code.
Brand customizes AuthServer.SeedAuthPage. Default page and email branding are seeded even when Brand is omitted. To let operators own new settings, set ConfigureAuthPageBranding = false and ConfigureEmailBranding = false and omit explicit branding seeds. Existing code-owned records become orphaned when their seed is removed; an authorized dashboard save can then claim them. See auth branding.
Authorization seeds vocabulary, not grants or enforcement. Your services must provision subjects/resources, assign roles during actual provisioning, and check access or filter queries on every operation. Never re-grant a role during ordinary reads after access was revoked. The Notes authorization model and EF authorization quickstart show both steps.
Move to the multiple-applications shape when you have more than one app experience, such as a customer portal, an admin console, a CLI, and a partner integration. Replace UseSingleApplication with ConfigureApplication, preserve the existing host description, and explicitly seed the original client with its existing ID, audience, callbacks, and scopes. Then use seeded clients or the dashboard to manage each application and use Application Access to control who can use each one.
For the product framing behind the simple path, read Single-Application Mode: WorkOS-Style Defaults Without Giving Up SqlOS.
Audience and application assignment stay separate: