> Our tech stack
Opinionated technology choices for a platform that needs to be reliable, fast, and simple to maintain.
Backend
.NET 10 + ASP.NET Core Razor Pages
backendServer-side rendering because it's the right tool for form-heavy business apps. No SPA complexity, no client-side state management, no hydration bugs. Razor Pages give us a clear request/response model with full-stack type safety.
Entity Framework Core
backendConvention-based ORM that handles migrations, change tracking, and query generation. Snake_case naming with Npgsql. Rich domain entities with private setters and factory methods.
FluentValidation
backendServer-side validation as the single source of truth. No client-side duplication. Inline field validation on blur via custom tag helpers.
PostgreSQL
backendReliable, performant, open source. Hosted on Azure Flexible Server with zero-downtime migrations via the expand-contract pattern.
Redis
backendHybrid cache (in-memory + distributed). Session state and output caching for the public-facing sites.
Frontend
Tailwind CSS v4
frontendUtility-first CSS with no custom component library to maintain. Design tokens via @theme. Dark mode via custom variant.
Alpine.js + Alpine-AJAX
frontendLightweight interactivity without a build step. Alpine-AJAX gives us partial page updates (like HTMX) with Alpine.js reactivity. No SPAs, no virtual DOM, no bundle size anxiety.
Infrastructure
Azure Container Apps
infrastructureServerless containers with built-in scaling, zero-downtime revisions, and managed certificates. Scale to zero in non-production.
Bicep
infrastructureInfrastructure as code that's actually readable. Every environment (production, test, PR previews) is defined in the same templates.
GitHub Actions
infrastructureCI/CD with PR preview environments, what-if analysis for infrastructure changes, and automated certificate bootstrapping.
Docker
infrastructureMulti-stage builds: Tailwind CSS compilation, .NET SDK for build, aspnet runtime for the final image.
Observability
OpenTelemetry
observabilityDistributed tracing and metrics. Vendor-neutral instrumentation.
Serilog + Seq
observabilityStructured logging with a beautiful local query UI. In production, logs flow to Azure Monitor.
Why Alpine-AJAX over a traditional SPA?
We tried the SPA approach. React, Next.js, client-side routing, state management libraries, API layers, serialisation, hydration, bundle splitting. All to render forms and tables for a business app.
The complexity wasn't justified. We were maintaining two applications (a .NET API and a React frontend) to do the job of one. Every feature required changes in both places. Type safety stopped at the API boundary. Client-side state drifted from the server. And the developer experience (waiting for webpack to rebuild while you tweak a label) was painful.
Alpine-AJAX gives us the UX people expect from SPAs (partial page updates, no full reloads, smooth transitions) without any of that overhead. The server renders HTML. Alpine-AJAX swaps fragments in the DOM. There's no JSON serialisation, no client-side routing, no state management, no virtual DOM diffing.
The result: one codebase, full-stack type safety, server-side validation as the single source of truth, and a frontend that's a few kilobytes of JavaScript instead of a few megabytes. Pages load fast. Forms just work. And we ship features in half the time.
-
$$
- _