Devafusion.net

Tech Stack

Every tool, one line each. Then the real reasoning.

Devafusion is a public proving ground for building to the highest standard we can hold ourselves to - every tool below is running in production today, chosen deliberately and revisited whenever a better option earns its place. Skim the verdicts in a minute; each one links straight to the dated ADR and engineering log entry that decided it, including what we rejected and why. Full commit history is public on GitHub.

Application & tooling

Next.js (App Router)

Production context: The entire web application - every route, from the marketing pages through to authenticated account/MFA flows - runs on Next.js's App Router with server-first rendering, deployed as a standalone Node.js build.

The technical thesis: Route-level metadata and a server-first default were needed from day one, ruling out a static-export-only build once an interactive health-check endpoint and (later) full authentication were already on the roadmap. Hosting was kept on Azure Linux App Service rather than Vercel specifically to avoid splitting infrastructure across two providers when everything else already runs under one Terraform-managed Azure tenant.

Dig deeper: ADR-0001 · engineering log

Terraform

Production context: Every Azure resource this project runs - App Service, PostgreSQL, Key Vault, DNS, email, cost controls - is defined and applied through Terraform. Manual Azure Portal changes are a zero-exceptions rule, not a guideline.

The technical thesis: Chosen over ARM/Bicep for its broader, multi-cloud-portable HCL ecosystem and more mature state-management model - keeping future vendor flexibility open even though this project is Azure-only today. Manual Portal configuration was rejected outright: it produces undocumented, unreviewable, non-reproducible infrastructure with no audit trail.

Dig deeper: ADR-0002 · engineering log

Drizzle ORM, drizzle-zod & zod-to-openapi

Production context: Owns every schema definition and query against the PostgreSQL database - identity tables, MFA state, rate limiting - sitting behind a repository-pattern interface so no page or component ever imports Drizzle directly.

The technical thesis: Chosen over Prisma specifically for a plain TypeScript schema (pgTable) rather than a second DSL to keep in sync by hand, and because Prisma's own newest migration engine is self-described as "early" where Drizzle's is mature and stable. drizzle-zod generates Zod validation schemas directly from the table definition, so the database schema, runtime validation, and OpenAPI spec can never drift out of sync by hand.

Dig deeper: ADR-0011 · engineering log

PGlite

Production context: Provides a real, in-process WASM PostgreSQL instance for local development and Vitest, with zero install step and zero production footprint.

The technical thesis: Neon was considered and explicitly rejected, even for throwaway schema testing - it would add a dependency on a second, unrelated third-party vendor's uptime, region, and free-tier terms for zero benefit over PGlite, while production data always stays in the Azure-hosted instance regardless.

Dig deeper: ADR-0011 · engineering log

Better Auth + a self-hosted TOTP layer

Production context: Owns session, credential, and OAuth core identity for every signed-in account. Multi-factor authentication is deliberately not handled by Better Auth's own plugin - it is self-built on top, using otpauth for RFC 4226/6238 TOTP generation and verification.

The technical thesis: Better Auth was chosen over Auth.js (NextAuth) because Auth.js's own documentation now states it is part of Better Auth - starting fresh on Auth.js today would mean planning a near-immediate migration. Better Auth's own twoFactor plugin was evaluated and rejected on a specific, documented finding: its own schema stores the primary TOTP secret as plain text, with no symmetric read-side decrypt hook its internal verification endpoint could use - encrypting that column via a write hook alone would silently break verification, not secure it.

Dig deeper: ADR-0012 · engineering log

Vitest + Playwright

Production context: A three-tier automated gate on every change: Vitest for unit and component tests, Playwright for end-to-end specs against a locally built standalone server, and a separate Playwright visual-regression suite that only ever runs inside the official, version-pinned mcr.microsoft.com/playwright Docker image.

The technical thesis: Running visual-regression screenshot assertions on the bare hosted CI agent (or a contributor's own OS) was rejected - font rendering differs across operating systems, producing false-positive diffs unrelated to any real regression. Chasing 100% coverage as a hard gate was also rejected in favour of honest coverage of real business logic over a vanity percentage.

Dig deeper: ADR-0007 · engineering log

Cloudflare Turnstile

Production context: Provides bot protection on sign-up, log-in, and password-reset - the only network-level (not header-derived) signal this project uses, deliberately in place of a paid Web Application Firewall or CDN.

The technical thesis: Azure Front Door Premium and Application Gateway both carry a real fixed monthly cost disproportionate to this project's current traffic. Higher-tier commercial bot-management products (Cloudflare Enterprise Bot Management, Akamai Bot Manager, DataDome) were named and considered, but rejected purely on cost/traffic-volume grounds, not technical suitability.

Dig deeper: ADR-0014 · engineering log

gitleaks

Production context: Scans every commit for secrets locally (a Husky pre-commit hook) and again server-side against the full pushed history on every CI run, before anything else executes.

The technical thesis: Fetched as the official, checksum-verified release binary into a git-ignored local cache, never through an unofficial npm wrapper package - consistent with this project's standing rule to prefer a tool's own official distribution channel over a third-party repackaging of it.

Dig deeper: engineering log

Azure resources actually provisioned

Every resource below exists in this project's own Terraform state today - none are planned-but-unbuilt.

Linux App Service + App Service Plan

Production context: Hosts the standalone Next.js server itself, on a B1 plan, with application logging deliberately set to its most permissive level ("Verbose") so console.error/console.log output is actually captured to disk.

The technical thesis: Started on B1 as a proportionate, low-cost baseline, with capacity reviewed as usage grows - not a fixed ceiling. Logging was flipped on after a real 500 on a live MFA route left zero server-side visibility into what had actually failed.

Dig deeper: ADR-0001 · engineering log

Azure Database for PostgreSQL - Flexible Server (Burstable B1ms)

Production context: The single relational store behind identity, MFA, and rate limiting, running PostgreSQL 16 with the citext extension explicitly allowlisted for case-insensitive email lookups.

The technical thesis: Chosen over Azure SQL Database and Cosmos DB for native JSONB support and standard wire-protocol portability. Burstable B1ms bills at a fixed hourly rate regardless of query volume - which is exactly why a separate Azure Budget circuit breaker exists as a safety net against human/automation error, not cost-from-load.

Dig deeper: ADR-0010 · engineering log

Azure Key Vault

Production context: Holds every runtime secret this project needs, read by the application and CI/CD pipelines at runtime - never written into a literal anywhere in code.

The technical thesis: Access-policy model (Get/List/Set/Delete, deliberately never Purge), soft-delete without purge protection - a resource removal always stays recoverable, rather than Terraform attempting an immediate hard purge the policy would reject.

Dig deeper: ADR-0004 · engineering log

Azure Communication Services (Email)

Production context: Sends every MFA one-time-code email and verification/reset link, from a custom-verified noreply@devafusion.net sender.

The technical thesis: Replaced an earlier Resend-based design rejected on UK GDPR data-sovereignty grounds - even Resend's nearest-to-UK region is a latency choice only, not a data-residency guarantee. Azure Communication Services' data_location setting is a genuine Microsoft-documented resource-level guarantee, and it's a first-party service already inside this project's own tenant.

Dig deeper: ADR-0015 · engineering log

Azure DNS Zones (three domains, full record set)

Production context: Three Azure DNS zones (.com, .net, .co.uk) carry every A, MX, TXT, and CNAME record this project needs: domain verification, SPF/DKIM/DMARC for two independent mail flows, Google Search Console verification, and an _atproto record verifying this site's Bluesky handle.

The technical thesis: Azure DNS treats every TXT value at a given name as one recordset, not independent records - a real incident (a second, colliding TXT resource at the same name) established the standing rule that multiple TXT values at one name must share a single Terraform resource block.

Dig deeper: engineering log

Domain registration (via azapi)

Production context: Registers and auto-renews all three domains directly through Azure's domain-registration resource provider, with WHOIS privacy enabled where the registry supports it.

The technical thesis: Uses the azapi provider since domain registration has no first-class azurerm resource type. The .co.uk registration explicitly sets privacy = false - Nominet does not support WHOIS privacy at all, and the API rejects true for that TLD outright.

Dig deeper: engineering log

Cost circuit breaker (Automation Account, Runbook, Action Group, Budget)

Production context: An Azure Consumption Budget on the resource group warns by email at 80% of actual monthly spend and, at 100%, triggers an Action Group that invokes a PowerShell Runbook to stop the PostgreSQL Flexible Server outright.

The technical thesis: Every component here stays entirely within Azure's free tier. This does not protect against a cost overrun from load alone (Burstable compute bills at a fixed hourly rate, full stop) - it protects specifically against a human or automation error that scales the tier up.

Dig deeper: ADR-0010 · engineering log

Azure DevOps Pipelines (CI/CD)

Production context: Two entirely separate pipelines - web and infrastructure - so a change to one never blocks the other. The web CI pipeline runs a gitleaks scan first, then builds, then Vitest, Lighthouse CI, Playwright end-to-end, and the Docker-pinned visual-regression suite. A separate CD pipeline applies pending database migrations behind a manual approval gate before deploying.

The technical thesis: The migration step runs unconditionally on every CD run, not gated on a git-diff of changed files - that gate was considered and rejected, since comparing only against the immediately preceding commit permanently loses track of a pending migration if any single CD run is ever rejected, times out, or fails downstream. drizzle-kit migrate is idempotent, so a deploy with nothing pending just finds nothing to do.

Dig deeper: ADR-0003 · engineering log

This site uses Google Analytics to understand traffic. See the privacy and cookies page for details.