Originally published on Medium.
A complete multi-tenant AI SaaS in C# and .NET 10: teams, Stripe billing, metered AI credits, RAG with citations and streaming chat. 101 tests, full source, $79.
Every AI product idea I've had lately started the same way. I'd get excited about the actual feature, the clever prompt or the document assistant, and then lose two weeks to everything around it.
Who belongs to which organization? What happens when someone gets removed from a team? How do I stop a free user from burning $40 of GPT tokens in an afternoon? Why did Stripe send subscription.updated before subscription.created, and why did my app just downgrade a paying customer?
None of that is the product. All of it has to work before anyone can pay you.
So I built it once, properly, and tested it. Today I'm releasing it as the AI SaaS Starter Kit for .NET 10: elitesolutions1.gumroad.com/l/ufvbep. For a limited time, the code ELITE40 takes 40% off.
What it actually is
It's not a template with a login page and a "your AI feature here" comment. It's a working, multi-tenant AI SaaS written in C# on ASP.NET Core 10, EF Core 10 and Microsoft.Extensions.AI 10.10. You rename it, plug in your OpenAI and Stripe keys, and customers can sign up, invite their team, pick a plan and pay monthly, while every token they use is counted against that plan.
The part I'm proudest of is small: it runs with no API keys at all. The default provider is a little offline model that streams real answers from your uploaded documents. Clone it, run dotnet test, run the app, sign in with the seeded demo account, and ask "How long do refunds take to arrive?" You get a streamed answer with a citation pointing at refund-policy.md. No account, no credit card, no surprise bill. When you're ready, switching to OpenAI (or any OpenAI-compatible endpoint) is one setting.
What's inside
Here's the honest list, grouped the way I think about it.
Tenancy that doesn't rely on you remembering a Where clause. Organizations with Owner, Admin and Member roles, invitations, and users who can belong to several orgs with a switcher. Every tenant-owned table gets an EF Core global query filter, and SaveChanges has a guard that refuses cross-tenant writes. Membership is re-checked on every request, so someone you remove loses access right away, not when their cookie expires.
Stripe billing that survives real webhooks. Checkout and the Customer Portal over plain REST (no SDK lock-in). Webhooks are signature-verified, recorded in an idempotency ledger, and protected against out-of-order delivery, so a late event can't roll a subscription back. Only your own Price ids can grant a paid plan. There's also a billing simulator, so you can build the whole upgrade flow before you even open a Stripe account.
Metered AI credits. Every model call goes through a DelegatingChatClient that bills the current organization. Plans get monthly credit allowances, models get per-token weights, and there's a live usage dashboard. When an org is out of credits it gets a 402 "upgrade required" before any streaming starts, and when it's nearly out, the answer is capped to what the balance can afford.
RAG with citations. Upload documents, and they're chunked, embedded and searched with cosine similarity. Answers carry numbered citations, and retrieved text is fenced off to blunt prompt injection. Moving to pgvector, Azure AI Search or Qdrant means replacing one method.
Streaming chat. Server-Sent Events using .NET 10's TypedResults.ServerSentEvents, conversation history, errors delivered inside the stream, and partial answers saved if the provider fails mid-reply.
A public API for your customers. Versioned /api/v1 endpoints with hashed, revocable API keys (shown once, like they should be), sharing the same credits and limits as the web app.
The boring production stuff. Per-plan rate limits with 429 and Retry-After, CSRF protection, a strict CSP, an audit log, health checks, OpenTelemetry, a Dockerfile plus docker-compose with PostgreSQL, and GitHub Actions CI. SQLite in development so there's zero setup.
101 xUnit tests, all passing. Unit tests plus full integration tests with WebApplicationFactory, covering tenant isolation, billing edge cases, metering, quotas, rate limits, invitations and API keys.
Two bits of real code
I'd rather show you than tell you it's well built. This is the streaming half of the metering client, straight from the project. The finally block is the whole point: it runs when the stream completes, when it fails, and when the user closes the tab halfway through. The provider charged you for those tokens either way, so the tenant gets billed either way.
public sealed class MeteringChatClient(IChatClient innerClient, UsageMeter meter, string feature = "chat") : DelegatingChatClient(innerClient)
{
public override async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
IEnumerable<ChatMessage> messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
var list = messages as IList<ChatMessage> ?? messages.ToList();
var text = new StringBuilder();
long? input = null, output = null;
string? model = null;
try
{
await foreach (var update in base.GetStreamingResponseAsync(list, options, cancellationToken).ConfigureAwait(false))
{
model ??= update.ModelId;
foreach (var content in update.Contents)
{
switch (content)
{
case TextContent t:
text.Append(t.Text);
break;
case UsageContent u:
input = (input ?? 0) + (u.Details.InputTokenCount ?? 0);
output = (output ?? 0) + (u.Details.OutputTokenCount ?? 0);
break;
}
}
yield return update;
}
}
finally
{
// Runs on completion, failure and cancellation alike: the provider charged for what it generated.
var estimated = input is null || output is null;
await meter.RecordAsync(
feature,
model ?? options?.ModelId,
input ?? EstimateInput(list),
output ?? CreditCalculator.EstimateTokens(text.ToString()),
estimated,
CancellationToken.None).ConfigureAwait(false);
}
}
// ... GetResponseAsync (non-streaming) does the same, minus the stream
}
If the provider reports token counts, it uses them. If it doesn't, it estimates and flags the record as estimated, so you can tell the difference later. Because it's ordinary Microsoft.Extensions.AI middleware, swapping providers doesn't touch the billing.
The second one is the tenant guard in the DbContext. Query filters stop you from reading another tenant's rows. This stops you from writing them, which is the bug that tends to show up at 2 a.m. in a background job:
private void StampTenant()
{
foreach (var entry in ChangeTracker.Entries<ITenantOwned>())
{
if (entry.State == EntityState.Added && entry.Entity.OrganizationId == Guid.Empty)
{
entry.Entity.OrganizationId = CurrentOrganizationId
?? throw new InvalidOperationException($"Cannot add {entry.Entity.GetType().Name} without an active organization.");
}
if (entry.State is EntityState.Added or EntityState.Modified or EntityState.Deleted &&
CurrentOrganizationId is { } current && entry.Entity.OrganizationId != current)
{
throw new InvalidOperationException(
$"Cross-tenant write blocked: {entry.Entity.GetType().Name} belongs to another organization.");
}
}
}
New rows get stamped with the current organization automatically. Anything that would touch another org's data throws. Code that genuinely needs to cross tenants, like the Stripe webhook handler, has to say so explicitly with IgnoreQueryFilters().
Who I built it for
Solo developers launching a micro-SaaS. You have an idea for an AI tool for lawyers, or landlords, or dental clinics. The kit gives you signup, teams, plans, payments and usage limits on day one, so the next weekend goes into the part your customers will actually notice.
Freelancers and agencies. "Build us an AI assistant that knows our internal docs" is one of the most common client requests right now. That's document upload, grounded answers with citations, per-company isolation and usage tracking, which is this kit. The commercial license covers unlimited personal and client projects. The only thing you can't do is resell the kit itself.
Teams that want a tested reference. Maybe you'll never ship this code as-is. That's fine. Having a working, tested answer to "how should multi-tenant AI billing work in .NET 10?" saves a lot of whiteboard arguments, and the 101 tests tell you exactly which edge cases were thought about.
The time math
I won't pretend there's a precise number, but here's my rough take. Getting tenant isolation right, with filters, the write guard, role checks and invitations, is a week for most people, and longer if you do it carefully. Stripe subscriptions with webhooks that are idempotent and handle out-of-order events is another week, and most first attempts get the ordering wrong. Token metering that also works for streamed and cancelled requests is a few days, plus the quota checks, plus the usage page. Then there are tests for all of it.
That adds up to weeks of work that no customer will ever thank you for, because when it works it's invisible. At $79 (or about $47 with the code) it's less than one billable hour for most of us.
To be clear about the limits: the kit doesn't include hosting, API credits or a Stripe account. The tests run against the offline model and the billing simulator, so you'll still want to do your own run against your real Stripe test keys and model provider before you go live.
Get it now
I'm genuinely excited about this one. It's the codebase I wish I'd had for every AI idea I started and abandoned at the "now add billing" stage.
Use the code ELITE40 for 40% off: about $47 instead of $79. It's a limited-time offer, so don't wait too long if you want it. And ELITE40 takes 40% off everything in my Elite Solutions store, the kit included, so it's a good moment to look around.
👉 Get the AI SaaS Starter Kit for .NET 10
Full C# source, 101 passing tests, and a commercial license. Rename it and ship something.
Want a new tested .NET + AI project every day, with full source? That's what Tech Skill Builder is for.
Top comments (0)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.