All posts
Engineering

How AzureProof is built: an honest tour of the stack

A plain-language walk through the architecture behind AzureProof — the server-rendered frontend, the Node.js evidence engine, the PostgreSQL database with row-level security, and the read-only pipeline that talks to Azure.

A
AzureProof Engineering
8/12/20269 min read
Engineering

Most "how we built it" posts are a wall of logos. A product whose whole job is to produce audit evidence should be able to show its own receipts instead. So this is a plain description of what AzureProof actually is: three parts, one database, and a hard rule that we can only ever read from your Azure tenant — never write.

The shape of the thing

AzureProof is one repository with three moving pieces:

  • The web app — a server-rendered React 19 application (TanStack Start) where you connect a tenant, watch runs, and download evidence packets.
  • The engine — a Node.js worker that polls a job queue, executes compliance checks against the Azure and Microsoft Graph APIs, and renders the PDF reports.
  • The control library — the 317 controls themselves, each one a small, tested detection function plus its own fix guide.

They share one PostgreSQL database, which is where the authorization actually lives (more on that below). Everything runs in Docker containers behind Nginx on a single Azure VM, with TLS from Let’s Encrypt.

The frontend: server-rendered on purpose

The marketing site and the authenticated dashboard are the same app, rendered with React 19 and TanStack Start. We render to HTML on the server first. That is not a performance vanity project — it means the landing page, the blog, and the docs are all readable before a single byte of JavaScript arrives, which matters for the people who actually read security pages: auditors, procurement, and search engines.

The styling is Tailwind CSS v4 with a small set of shared motion primitives (scroll reveals, spotlight cards, a count-up). Every animation only moves transform and opacity, and each one degrades to a static element under prefers-reduced-motion.

The engine: a worker that only ever does GETs

The engine is the part that does the work. It polls a queue for claimed runs, then executes the requested controls. Each control is a TypeScript function that:

  1. Acquires a token for the customer’s tenant,
  2. Issues read-only API calls against Azure Resource Manager or Microsoft Graph,
  3. Returns a status — pass, fail, or n/a — plus the raw API response as evidence.

There is no create, update, or delete path anywhere in the engine. The service principal we ask you to create is scoped to Reader and Security Reader on the subscription, plus five read-only Microsoft Graph permissions. The raw response behind every result is stored so an auditor can trace a conclusion all the way back to the API call that produced it.

After a run completes, the engine renders the evidence packet as a PDF. Those PDFs are generated at run time from the stored results, not hand-assembled.

The control library: small, tested, and honest

There are 317 registered controls. Of those, 221 perform a real Azure API check. The remaining 96 report n/a because no automated check exists for them yet — we explain why that is a feature, not a gap, in a separate post.

Each automated control has a unit test, and the whole suite runs on every change. At the time of writing that is 689 engine tests, and they are part of the deploy gate.

The database: authorization in PostgreSQL, not in the UI

The frontend can hide buttons, but hiding UI is not security. The real authorization lives in the database as PostgreSQL row-level security (RLS) policies. A user can only see the organizations they belong to; a tenant’s encrypted credentials are not even selectable from the browser; and the write paths that forge evidence or upgrade a plan are locked to the service role.

Tenant secrets are encrypted with AES-256-GCM before they are stored, and are only ever decrypted server-side to run a collection. We never hold them in the browser.

How it all ships

Every change flows through the same pipeline: typecheck, the full test suites, a Trivy vulnerability scan, a database security gate, and a health check after deploy with an automatic rollback if it fails. That pipeline deserves its own post, so here it is.

If you want the shorter version with diagrams, head to the How it’s built page. If you want to see what the engine finds in your own tenant, connect one and run your first collection.

Get new posts in your inbox

One thoughtful email per month on Azure compliance. No spam.

See it in your own tenant

Connect a read-only service principal and run your first evidence collection the same day.

Start free trial