Skip to content
TechFabric

LakeWright.NET carries a SaaS tenant boundary into Databricks

Ivan Vydrin7 min read

The LakeWright.NET architecture at a glance: an ASP.NET Core application plane resolving a tenant, a Databricks data plane governed by Unity Catalog, and a delivery plane deploying both as code.

LakeWright.NET carries a resolved SaaS tenant into Databricks SQL, Jobs, AI/BI, Genie and Model Serving without asking customers to enter your workspace.

The hard part of putting Databricks behind a B2B product isn't reaching the warehouse; it's preserving the customer boundary after a request leaves your application.

Your product knows Alice belongs to Acme Logistics. Databricks may only see the service principal your backend uses, and if that principal serves every customer, the platform identity is identical for Acme and Globex. A Unity Catalog row filter can enforce rules for the identity Databricks receives, but that shared identity can't distinguish the two product tenants by itself.

I built LakeWright.NET for that gap. It's an Apache-2.0 set of .NET libraries, a reference architecture and a working sample for customer-facing analytics on Databricks. The application checks membership, creates a tenant context and carries the tenant's catalog and schema into every SQL statement and long-running operation.

Databricks remains the data and AI plane. LakeWright supplies the application boundary around it.

The identity changes at the application boundary

Two approaches are sound. One passes the person's platform identity through to Databricks, and the other lets an application identity reach Databricks and enforce the product tenant before the call leaves the application.

Databricks Apps now supports user authorization and is a strong fit when users belong to the Databricks account. LakeWright targets a different topology: the end user signs in to your SaaS product, may never have a Databricks identity, and expects your domain, interface and account model. The .NET application therefore runs outside Databricks and uses Databricks as a governed service behind it.

LakeWright doesn't compete with Databricks Apps. The two solve different hosting and identity problems. Internal tools can stay native to the workspace. A customer-facing .NET product can keep its own application tier while using Databricks SQL, Lakeflow Jobs, AI/BI dashboards, Genie and Model Serving.

Where LakeWright fits

LakeWright separates the system into three planes.

The application plane contains ASP.NET Core, OIDC, the product's users and organizations, the resolved tenant context, PostgreSQL state and the audit trail. The data plane stays on Databricks, with SQL warehouses, Delta tables, Lakeflow Jobs, AI/BI, Genie and Model Serving governed by Unity Catalog. The delivery plane uses Declarative Automation Bundles and CI to validate and deploy the Databricks resources as code.

The split is deliberate. Databricks already provides the compute, governed data services and AI surfaces. Rebuilding any of that in a .NET framework would create a weaker duplicate. LakeWright concentrates on the product concerns that begin before the Databricks API call: membership, tenant ownership, idempotency, durable operation state and audit evidence.

Two planes side by side. On the left, the .NET application holds identity, membership, tenant context and operation state. On the right, Databricks holds SQL warehouses, Delta tables, Jobs, AI/BI, Genie and Model Serving under Unity Catalog.

LakeWright handles application identity and state. Databricks provides governed data and AI.

One tenant context per request

The central type is TenantContext. An application can't construct one from a tenant ID supplied in a URL, because a registered resolver must first confirm that the authenticated principal belongs to that tenant.

Once resolved, the context carries the Unity Catalog catalog and schema. TenantScopedStatement requires that context, and the executor accepts no unscoped statement type. The catalog and schema therefore come from trusted membership data rather than from request parameters.

var statement = TenantScopedStatement.Create(
    tenant,
    "SELECT * FROM orders WHERE status = :status",
    StatementParameter.String("status", "OPEN"));

await executor.ExecuteAsync(statement, cancellationToken);

Values use typed parameters from the Databricks Statement Execution API. Direct interpolation into the statement factory is a compile error. The code still treats dynamically concatenated SQL as unsafe, because a useful security claim includes the residual risk instead of pretending the compiler can prove more than it does.

The recommended default is one Unity Catalog schema per tenant. The isolation boundary still depends on a correct, trusted mapping from each tenant to its catalog and schema. LakeWright also documents catalog-per-tenant for customers who need a stronger isolation tier, and a source-owned policy model for very large shared-table deployments. The generic SQL API deliberately doesn't claim that it can make arbitrary shared-schema SQL safe.

A request flow. The membership check reads the tenant's catalog and schema and passes them to the statement executor. A request whose principal has no membership in that tenant stops at the check.

A membership check supplies the catalog and schema. A request without matching membership is rejected.

Databricks remains the platform

LakeWright is intentionally thin at each Databricks boundary.

  • SQL queries use the Statement Execution API with typed parameters, inline results for application-sized reads and external links for larger exports.
  • Long-running work uses Lakeflow Jobs. The application returns 202 Accepted, stores the operation in PostgreSQL and polls the Databricks run outside the HTTP request.
  • AI/BI external embedding uses the Databricks scoped-token flow. LakeWright binds the token request to the resolved tenant rather than accepting a tenant value from the browser.
  • Genie conversations keep tenant and conversation ownership in the application, then call the Databricks Genie API for the actual analysis.
  • Model Serving is exposed through Microsoft.Extensions.AI.IChatClient, so a .NET application can use the familiar abstraction while Databricks hosts the endpoint.
  • Declarative Automation Bundles define the Databricks resources and permissions that belong in source control.

The durable operation path is a good example of the partnership between the layers. A client sends an idempotency key. LakeWright stores the pending operation and returns immediately. A worker submits the Databricks job with a server-generated idempotency token, records the run ID and resumes polling after a process restart. Databricks guarantees that the same token doesn't launch a second run. LakeWright connects that platform guarantee to the tenant-owned operation the customer sees.

That matters because duplicate analytics work isn't only a reliability bug. The tenant may pay for both runs.

Try the boundary without a workspace

The Signalboard sample makes the tenant rule visible before any Databricks resource is involved. Docker builds the .NET application, starts PostgreSQL and seeds two organizations with three identities.

git clone https://github.com/ivanvyd/LakeWright.NET
cd LakeWright.NET/samples/Signalboard
docker compose up

Open http://localhost:8080. Alice is an Acme administrator and can start work. Vera, also at Acme, is a viewer and can read but not start one. Bob belongs to Globex and can't retrieve an Acme operation even if he knows its identifier. The API returns 404, not 403, because a forbidden response would confirm that the resource exists.

No Databricks account is required for this part. Without a workspace, operations stay pending. The point of the local sample is to let you test membership, roles, ownership and the HTTP boundary in a minute.

Three identities across two organizations. An Acme administrator can start an operation, an Acme viewer can only read it, and a Globex user requesting the same operation by its identifier receives a 404.

Roles control actions within a tenant. A cross-tenant lookup returns 404.

Add LakeWright to an ASP.NET Core application

The stable v2.1.0 packages are published on NuGet. Start with the application integration package and add the Databricks package when the process will execute SQL or Jobs.

dotnet add package LakeWright.AspNetCore --version 2.1.0
dotnet add package LakeWright.Databricks --version 2.1.0

Registration follows the order of the request pipeline.

builder.Services
    .AddAuthentication()
    .AddOpenIdConnect();

builder.Services.AddLakeWright(builder.Configuration);
builder.Services.AddLakeWrightDatabricks(builder.Configuration);
builder.Services.AddLakeWrightOperationWorker(builder.Configuration);

var app = builder.Build();

app.UseAuthentication();
app.UseLakeWrightTenancy();
app.UseAuthorization();
app.MapLakeWrightOperations();

UseLakeWrightTenancy sits between authentication and authorization because the policies read the tenant it resolves. If the product already has a trusted membership store, it can replace LakeWright's PostgreSQL resolver and keep the rest of the boundary.

On Azure, the reference path registers DefaultAzureCredential and uses managed identity to obtain the Databricks token. The repository also includes a Databricks bundle for the workspace side and a Bicep reference template for Azure Container Apps.

What v2.1.0 proves

LakeWright is early software, and the repository is unusually explicit about that. Its compatibility matrix separates live verification from vendor documentation and inference.

The v2.1.0 release evidence records 465 non-live tests, including 401 tenant-isolation tests, package-consumer checks, vulnerability scanning, CodeQL, bundle validation and a signed release pipeline. Live Azure Databricks checks have covered typed Statement Execution parameters, job submission and idempotency, external result links, AI/BI external embedding, Genie conversations and Model Serving. The stable release contains 13 package surfaces.

The gaps are just as important. The Azure deployment template compiles but hasn't been used for a reference production deployment. Currency billing attribution is implemented and locally tested, but its system-table read hasn't been live-verified. AWS and GCP remain unverified. Live tests require a workspace and cost money, so they don't run in public CI.

The project also doesn't claim to be a Databricks SDK, a generic SaaS starter or a replacement for the Databricks AI/BI client. It depends on an existing .NET Databricks client and adds the tenant boundary around selected APIs. There's no SLA, and the project is independent rather than a Databricks product.

The credible claim is narrower. The code gives a .NET team a tested starting point for carrying a customer identity boundary into Databricks without weakening the Databricks platform underneath it.

A narrower layer makes a better integration

LakeWright stays useful by staying narrow. It doesn't move governed data out of Databricks or replace Unity Catalog, Jobs, AI/BI, Genie or Model Serving. It makes those services usable from a customer-facing .NET application whose tenant model lives somewhere else.

For a product team, this division keeps analytics and AI on Databricks while tenant membership stays in the application database. The boundary is explicit in code, and each platform keeps the responsibility it can enforce.

LakeWright.NET is Apache-2.0 licensed. The sample, packages, architecture decisions, threat model, compatibility matrix and release evidence are public at github.com/ivanvyd/LakeWright.NET and lakewright.net.

Sources

All sources were checked on 22 September 2026. LakeWright implementation claims were compared with the public source tree and release evidence. Databricks capability claims were checked against current Databricks documentation.