A boundary the caller cannot reach.
- What it is
- A reference architecture and seven packages — not a framework you adopt whole.
- Who for
- .NET teams selling analytics to customers who are not themselves Databricks customers.
- Status
- Prerelease — 0.1.2-preview.1, Apache-2.0, .NET 10. Seven packages, published on NuGet.
Unity Catalog row filters resolve the caller with session_user(). One service principal for every customer means one predicate for every tenant. LakeWright.NET moves tenant isolation into the query layer — open source, Apache-2.0, for .NET teams on Databricks.
demo|bob is an Admin. His request still stops at the boundary — not refused, no path. Membership resolves before authorization runs, so the answer is 404. A 403 would confirm the work exists.
Nothing here is a mock-up. Every identity, status code and row comes from the Signalboard sample in the repository, and you can send the same requests yourself in two minutes.
Pick a conductor above. The response, the pipeline and the register all answer as that person — the request is printed in full below.
Sending as Acme Logistics member demo|alice.
The default breaks quietly. This is what it looks like.
R-01Read one operation
A member of Acme. The tenant resolved, so the store answers.
R-02Start work
Accepted. Send it twice with the same key and you get this operation back, not a second Databricks run.
- Step 01AuthenticateThe demo scheme believes demo|alice.
- Step 02Resolve tenantMembership found. TenantContext issued for Acme.
- Step 03Authorize roleAdmin. A floor inside the tenant, not the boundary.
- Step 04Operation storeFilters on the resolved tenant. Ten rows.
-- Unity Catalog's own syntax. Evaluated as the-- connected principal, not as your end user.CREATE FUNCTION tenant_rows(tenant_id STRING)RETURN tenant_id = tenant_of(session_user());ALTER TABLE events SET ROW FILTER tenant_rows ON (tenant_id);
One backend, one service principal, every request. The predicate is identical for all of them: every tenant sees everything, or every tenant sees nothing. Both look like a working product from outside, which is why this is found in an audit rather than in testing.
Membership is read from the database on every request and resolves before authorization runs. The request stops at resolution, and the query layer has no overload that omits the tenant.
Vera keeps every row and loses one control, and the API refuses her write too — a hidden button is not an authorization control. Roles are a floor inside a tenant, never the boundary.
| Started | Kind | State | By | Operation |
|---|---|---|---|---|
| 11:34:25 | analysis | Pending | Alice | 019fbd1a-9c30-7af8-b9fd-0843432ca47f |
| 11:31:55 | analysis | Pending | Alice | 019fbd18-5197-73f6-b2ca-7f4057f50f08 |
| 11:16:23 | analysis | Pending | Alice | 019fbd0a-1927-78cf-a121-6259c768b649 |
| 11:13:30 | analysis | Pending | Alice | 019fbd07-750b-7668-9301-2ce475819a60 |
| 11:12:17 | analysis | Pending | Alice | 019fbd06-585f-7941-ba09-147576a14605 |
| 11:10:43 | analysis | Pending | Alice | 019fbd04-e855-7154-bce3-f00bde756fe1 |
| 11:06:19 | analysis | Running | Alice | e369391d-8e48-44ae-a112-69f78993f0ca |
| 10:51:19 | export | Failed | Alice | 46c22a0c-571f-45f1-b37c-62b0190bd006 |
| 10:37:19 | export | Succeeded | Vera | 4ff8a9d2-e029-4c3e-9635-84cf0b68a806 |
| 10:24:19 | analysis | Succeeded | Alice | 4aff9092-fad3-499d-8962-8f96cfbc1f16 |
Ten rows, because ten belong to Acme Logistics. The dashboard reads through the same resolver and the same store as the API.
There is no register here to show. Membership resolved before authorization ran and found none, so the request stopped at tenant resolution. The rows were never queried, never filtered, and never refused. As far as this session is concerned they do not exist.
A real interface, photographed, not drawn.
A browser smoke test drives the sample and takes these. Nothing is retouched, and every plate opens its full file.

Every row Acme has, and one control she cannot use — the reason printed beside it rather than the button hidden.

Same application, same code path, different membership. As far as Bob's session is concerned, Acme's work does not exist.
Hover a plate to straighten and lift it. C and D are printed over-size and cropped to the top-left, because a full-width thumbnail of a 1280-pixel screenshot is unreadable.
Seven packages, and nothing you must take.
Take 01–04 for tenancy and the query layer. 05–07 are brokered access, kept separate so a team who does not want them never compiles them. There is no meta-package.
- 01LakeWright.CoreTenantContextTenancy and job contracts. TenantId, TenantContext, IJobSubmitter, RunOutcome.no dependencies
- 02LakeWright.MultitenancyOperationStoreTenant model and resolution, tenant provisioning and deletion, the operation store, the append-only audit trail, EF Core and database hardening. The worker survives a crash mid-run: claims are recovered and polling resumes, rather than starting a second Databricks run the tenant also pays for.no Databricks
- 03LakeWright.DatabricksTenantScopedStatementTenant-scoped SQL and job access. Catalog and schema come from the resolved tenant context, and interpolated SQL does not compile.the workspace
- 04LakeWright.AspNetCoreTenantResolutionMiddlewareTenant middleware, role policies, and the operations API. A retried request returns the original operation; the same idempotency key with a different kind answers 422 rather than the wrong answer.the edge
- 05LakeWright.EmbeddingAI/BI scoped tokensScoped tokens for AI/BI dashboards. It brokers the token that flow needs and stops there; rendering stays the embedding client's job.optional
- 06LakeWright.AIIChatClientDatabricks model serving as an IChatClient.optional
- 07LakeWright.ConversationsTenant-scoped GenieGenie conversations, scoped to the resolved tenant.optional
Enforced by a test, not a convention
LakeWright.Multitenancy does not reference LakeWright.Databricks, so adopting the tenancy tier alone does not drag in the Databricks client. DependencyDirectionTests fails if that changes.
Does not compile
Passing an interpolated string where SQL is expected does not compile. Values go in as StatementParameter arguments; catalog and schema come from the tenant context and are validated as identifiers.
Secretless, with one named exception
You register a TokenCredential, not a string — Entra tokens expire within the hour and the credential is what knows how to get another one. One optional module is the exception: LakeWright.Embedding needs a service principal OAuth secret, because Databricks documents no other credential for the AI/BI external-embedding token exchange — so an application that embeds dashboards holds one long-lived secret.
Docker is the only requirement.
No .NET SDK, no Databricks workspace, no account — tenancy and authorization never talk to Databricks, so the isolation model runs without one. The sample seeds Alice, Vera and Bob, so the test above is the one you run locally.
git clone https://github.com/ivanvyd/LakeWright.NETcd LakeWright.NET/samples/Signalboarddocker compose up# then open http://localhost:8080 and sign in# as Alice, Vera or Bob
builder.Services.AddLakeWright(config);builder.Services.AddLakeWrightDatabricks(config);builder.Services.AddLakeWrightOperationWorker(config);app.UseAuthentication();app.UseLakeWrightTenancy();app.UseAuthorization();app.MapLakeWrightOperations();
Order matters. UseLakeWrightTenancy() sits between authentication and authorization, because it resolves what the policies read.
Working today
- Tenant model and resolution, provisioning and deletion
- Tenant-scoped Databricks query layer
- Async operation worker — crash reconciliation, resumed polling
- ASP.NET Core middleware, role policies, operations API
- Client idempotency
- Append-only audit trail
- Declarative Automation Bundle
- Telemetry instruments — no tenant id on any metric
- The Signalboard sample
Still open
- Cost attribution in currency, and per-tenant token metering
- An observability export
- A reference deployment
Deliberately not this
- Not a Databricks SDK — it depends on Microsoft.Azure.Databricks.Client
- Not a dashboard embedding library — AI/BI external embedding ships today; use it
- Not a generic ASP.NET Core SaaS starter. If you are not on Databricks, nothing here is for you
- Not an admin portal, a workspace provisioner, or a notebook tutorial
Read the code. That is the point.
Every identity, status code and row on this page comes from the Signalboard sample in the repository. Clone it and send the same requests yourself.

