.NET 10 + Razor Pages
Server-side rendering because it's the right tool for form-heavy business apps. No SPA, no client-side state, no hydration bugs. Entity Framework Core and Npgsql carry the same type safety all the way to the database.
Opinionated technology choices for a platform that needs to be reliable, fast, and simple to maintain.
Backend
Server-side rendering because it's the right tool for form-heavy business apps. No SPA, no client-side state, no hydration bugs. Entity Framework Core and Npgsql carry the same type safety all the way to the database.
Reliable, performant, open source. Hosted on Azure Flexible Server, with zero-downtime migrations via the expand-contract pattern.
Joey runs on OpenAI models hosted in Azure AI Foundry, processed in the EU and never in the US. He reads documents, drafts enquiries and takes quotes on live calls.
Clients get their updates where they already chat. Messages go out on every milestone, and replies land in a shared inbox the team works from.
Frontend
Utility-first CSS with design tokens via @theme and dark mode via a custom variant. It styles Koala's shared component library, so every site and screen is built from the same tag helpers instead of one-off markup.
Lightweight 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
Container Apps and jobs for every service, PostgreSQL, Blob Storage, Service Bus and Key Vault, all described in Bicep templates. Calls and email run on Azure Communication Services. Production, test and every PR preview come from the same templates.
CI/CD with PR preview environments and what-if analysis for every infrastructure change.
Multi-stage builds: Tailwind CSS compilation, .NET SDK for build, aspnet runtime for the final image.
Observability
Every production error and log lands in Sentry, tagged with the release that caused it. Locally, Serilog, OpenTelemetry and Seq show the same story.
Product analytics that show where people get stuck, so we fix the screen rather than guess.
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.