I spent the last eight months of nights and weekends building an AI-agent platform to teach myself AI properly, after 20+ years as an enterprise architect. The product turns documented procedures — SOPs, policies, runbooks — into working agents exposed as API endpoints, with per-tenant isolation, spend caps, scoped keys, plan-based limits, and full run auditing. One .NET solution: EF Core, a handler layer, Azure OpenAI behind a provider abstraction so a workspace can swap to Anthropic, Gemini, or its own key.
The AI turned out to be maybe 20% of the work. This post is about the other 80% , the architecture decisions that made it safe to hand to strangers, and what they cost me to learn.
One enforcement choke point beats many gates. Every agent-creation path — the REST API, a template gallery, a guided builder, and a "describe it in plain English" mode — funnels through a single handler. Plan limits (agent counts per tier) and validation live in exactly one place. When I added a fifth creation path late in the build, it inherited the limits for free. If enforcement had lived per-endpoint, I'd have shipped a bypass without knowing it.
Enforce at the boundary every run passes through — before the first model call. Spend ceilings started life on the public API only. That left the interactive console and scheduled runs uncapped: a free user could sit in the workbench and burn my Azure bill. The fix was moving the check into the one method every run type flows through, evaluated before any model call — including the planning call, which is itself billable. A whole category of "why didn't that get blocked" bugs disappeared in one commit.
Plan entitlements as one rules table. Agent counts, API access, scheduling, connectors, seats — all resolved from a single static PlanEntitlements type. The pricing page, the API's 402 responses, and the UI banners can never disagree about what a tier includes, because there's only one place to ask. Corollary I nearly learned publicly: your pricing page is a promise your code has to keep. I almost launched with "15 agents" bullets that nothing enforced.
Tenant isolation via global query filters — with audited exceptions. EF Core's global query filters scope every query to the current tenant by default. The deliberate escapes — IgnoreQueryFilters() — appear only in the platform-admin views that are meant to see across tenants, which makes them greppable in code review: every occurrence should justify itself.
Your test surface must run the production path. My workbench originally re-planned an agent's steps conversationally instead of executing its published procedure. Test behavior diverged from live in ways that took three separate debugging sessions to pin down. The fix routed the workbench through the same execution path as a production API call. If your test surface exercises a parallel path, you're testing a different product.
Errors die in the client more often than you think. My SSE chat surface rendered server errors correctly — and then the stream-close finalizer overwrote them with empty text. Every server-side failure looked like a blank reply, and I re-debugged the server twice before suspecting the client. If your UI "shows nothing," check the finalization path before touching the backend.
The platform is live with a free tier and an anonymous demo if you want to see the result. But mostly I'm curious how others structure multi-tenant limit enforcement and provider abstraction in .NET and what patterns are you using?
Platform: https://studio.tramenterprise.com
Top comments (0)