Obliga · technical summary

How Obliga is built

A single-tenant .NET 10 / Blazor Server / Azure SQL application. One instance and one database per customer, with isolation by deployment rather than by a tenant column. This page is written for people who will evaluate that, including the parts that count against it.

Obliga is a focused product, not a platform. It keeps a governed record of the contracts you've signed and makes sure a renewal or notice deadline never passes unnoticed. It doesn't do drafting, e-signature, or negotiation. It only makes sense for organizations already on Microsoft 365, because it runs on what you already have: staff sign in with their existing accounts, the people list comes from your directory, and reminders are sent from your own mailbox. Those reminders only ever go to your own staff. Obliga never emails a counterparty.

Stack

Runtime & platform

App.NET 10, Blazor Server, MudBlazor
DataEF Core (Microsoft's object-to-database mapper) over Azure SQL. Every database operation opens its own short-lived connection through IDbContextFactory, rather than sharing one long-lived DbContext handed out by dependency injection. In Blazor Server a user's session stays open for hours, so a shared one would be held for that entire time and used by overlapping operations, which EF Core explicitly doesn't support.
IdentityEntra ID (OIDC) for sign-in; Microsoft Graph for directory sync and mail
SecretsManaged identity wherever Azure allows it. The interactive sign-in registration is the one place a client secret is genuinely required; it lives in Key Vault, never in configuration.
InfrastructureBicep, Azure's infrastructure-as-code language, applied with the az command-line tool. The whole environment (app, database, storage, key vault, permissions) is described in version-controlled files and created by running a script, rather than assembled by hand in the Azure portal. The same templates deploy onto infrastructure we run, or into your own Azure subscription.
HostingOne App Service and one Azure SQL database per customer. Databases share a logical server; apps share an App Service Plan.

Layout

Three projects, one direction of dependency

Obliga.Web
The user interface: Blazor Server pages, and the single startup file that wires the application together
Obliga.Infrastructure
Everything that talks to the outside world: the database via EF Core, schema migrations, Microsoft Graph, sending mail
Obliga.Domain
The rules themselves: the contract model and the reminder-scheduling engine. Touches no database and no network.

Each layer depends only on the one below it, and Domain depends on nothing at all. That matters for one reason. The reminder-scheduling engine lives there: working out which dates a notice fires on, counting business days, handling a decision that pauses the schedule versus one that ends it, and the failsafe window that catches a deadline nobody acted on. It's the part of Obliga where a subtle bug doesn't throw an error; it just quietly fails to remind someone. Because it's plain C# with no database or network involved, thousands of date scenarios can be tested in seconds, which is why most of the test suite points at it.

Guarantees

Four things the code enforces, rather than asks you to remember

These four are enforced by the structure of the code, not by a developer remembering:

  1. 01
    Reminders can't be sent twice.Every send is recorded against a unique key (contract, notice point, recipient, scheduled date), and the database rejects a duplicate. So if the nightly job crashes halfway and runs again, or gets triggered twice by accident, nobody receives the same reminder twice. That's enforced by a database constraint, not by careful code.
  2. 02
    History can't be rewritten.Every field-level change is written in the same database transaction as the change itself, so a change and its audit record either both happen or neither does. The data layer then refuses any attempt to modify or delete an audit row. No code path, including one written years from now, can quietly edit the trail.
  3. 03
    Every email leaves through one door.Reminders, weekly digests, and failure alerts all pass through a single component, and that component is where test mode is checked. With test mode on, every message is redirected to a test inbox with a marked subject line. There is no second sending path to forget about, so "we were testing and it emailed real people" isn't a mistake the shape of the code allows.
  4. 04
    Permission is always person + role + scope.Every grant says "this person holds this role, for this department, or for all of them", and one service answers every permission question. What a user is allowed to see is then applied as a filter at the database level, in one shared place used by the grid, dashboard, contract page, console, and exports, so those screens can't drift apart and start showing different things.

Beyond those, the codebase holds itself to conventions that are genuinely conventions: configuration over hardcoded business constants, UTC timestamps with date maths isolated in one service, consistent keys and naming. They are documented and reviewed, but honesty requires saying that they are enforced by discipline rather than by the compiler.

Trade-offs

What to weigh before you evaluate further

Blazor Server means a live connection

UI state lives on the server and each user holds an open WebSocket. That buys a small, simple codebase with no separate API surface, and it costs you offline tolerance. A flaky connection means a visible reconnect. It suits staff working from an office or home broadband, and suits a field workforce on patchy mobile data far less.

One instance per customer, deliberately

Three background jobs run inside the application itself: sending reminders, sending the weekly digest (a summary email giving each contract manager their whole portfolio at a glance), and syncing staff from your directory. Nothing coordinates which copy of the app should run them, so each customer runs a single copy. The upside is strong isolation and simple reasoning about what is happening. Azure's 99.95% availability commitment applies to that single instance, so expected uptime is high; the practical cost is that a deployment or a platform restart is a brief interruption rather than a seamless one, which is why updates happen in an agreed window.

Microsoft 365 is required, and safer for it

Your staff list is read from Microsoft Graph, and reminders are sent from a shared mailbox in your own tenant. There is no other way to get people into the system and no SMTP alternative, so Obliga cannot run without it.

That same dependency is what keeps your data safer. Obliga never stores a password, so it holds no credential of yours to lose. Sign-in happens in your tenant, so the multi-factor, conditional access, and device rules your IT already enforces apply here with nothing to configure. Disable an account on someone's last day and they lose access to Obliga at that moment, with no second account to remember. Reminders leave from your own mailbox, so they stay inside your retention and DLP rules rather than being sent on your behalf by a third party.

Deployment is scripted, not yet continuous

Setting up a new environment is done with Bicep, Azure's infrastructure-as-code language, so the servers and databases are described in version-controlled files rather than clicked together by hand. Releasing new application code is a packaged build pushed to Azure. Both are repeatable and reviewable, but neither is yet a CI/CD pipeline: the automated chain that builds, tests and releases every approved change on its own, with checks that block a bad release. Today a person runs the release step. That is on the roadmap.

Who builds it

One experienced developer, working deliberately

Obliga is designed and directed by Ian Johns, and built with Claude Code (Fable, Opus 5, Sonnet 5). Ian is an application developer with decades of experience in software design, consulting, and technical architecture, specialising in simplifying complex systems and delivering scalable, maintainable solutions. The background is squarely Microsoft: Power Platform, SharePoint, Dataverse, Teams, .NET, and SQL Server, across both cloud and on-premises architecture, and across the full project lifecycle from design through delivery and support.

That matters here for one reason in particular: Obliga is a small product with a narrow job, and the risk with small products is not that they lack features, it is that nobody thought carefully about the data model, the failure modes, or what happens on the day something goes wrong. Those are the parts that got the attention.

It is also worth being direct about what that division of labour means, because it is a fair question to ask in 2026. The architecture, the standing rules, and the design brief are defined and maintained by hand. Those rules live in the repository and constrain what gets written; every change is reviewed, built, and tested before it ships. The result is that a solo developer can move at a pace that would normally need a team, without the codebase turning into something nobody understands. The judgement about what to build, what to refuse, and what counts as correct remains a human one.

Exit

Getting your data out, and what happens if we are not here

Your data is yours, and it comes out without anyone's permission. Everything below is a button in the product rather than a support request, so there is no ticket to raise and nobody to wait for.

Some of it. Any view of the contract grid exports to CSV or Excel, matching whatever filter, search, or saved view is on screen. That is the everyday case: give the auditor the twelve agreements they asked about, or hand a department its own list. The same view also exports as a zip that includes the documents, so what you send is the records and their files together rather than a spreadsheet and a promise to follow up.

Or all of it. One button on the Admin screen takes the complete record: every contract whatever its status, active and closed and cancelled together, with every attached document, as a single archive. A CSV of the metadata, then one folder per contract holding its files. Moving a portfolio of hundreds of documents to another system is one download rather than hundreds, and there is nothing to ask us for. Separately, an administrator can export the full field-level audit trail for a compliance record.

Each archive carries a short readme describing exactly what is in it and how it was produced, because "is this everything?" is the first question anyone asks of an export they have been handed.

The other fair question, given that Obliga is built by one person, is what happens if that person is not available. Source code escrow can be arranged on request: an independent agent holds a copy of the source and releases it to you under conditions agreed in advance, such as the business ceasing to trade or failing to meet its support obligations. Very few customers ever need to call on an escrow agreement. It should still be there for the asking rather than something you have to think to negotiate for.

361 automated tests, weighted heavily toward the scheduling engine's business-day and cadence maths, since that is the one place a subtle bug silently costs a customer a missed renewal rather than showing an error.

Most of those run against EF Core's InMemory provider, a stand-in database that lives inside the test process. It makes tests fast, but it is not SQL Server, and it quietly tolerates things a real database rejects. So anything that depends on real query behaviour is additionally checked against an actual SQL Server. That difference has caught genuine bugs here more than once.

← All Obliga guides