DEV Community

azhadsuhaimi
azhadsuhaimi

Posted on

Why I Built a .NET 8 + Next.js SaaS Boilerplate (And 4 Architecture Decisions I Made)

Building a full-stack SaaS application from scratch is usually fun—until you reach the part where you have to wire up identity managers, ORM migrations, CORS policies, OAuth redirects, and payment webhooks.

Over the past two months of building at PulseLabs, I found myself solving the exact same foundation problems repeatedly. To eliminate this friction permanently, I engineered NetPulse: a production-ready starter kit leveraging .NET 8 Web API, Next.js App Router, PostgreSQL 16, and Docker.

Instead of a generic feature walkthrough, I want to share the 4 core architectural decisions I made during this build and why I structured it this way.


1. Enforcing Clean Architecture in the Backend

Many SaaS templates bundle all backend logic directly inside API controllers or Next.js route handlers. While fast initially, it quickly turns into spaghetti code as the business logic grows.

For NetPulse, I separated the backend into 4 distinct projects within a single solution:

NetPulse.sln
├── Core/
│   ├── Domain/         # Pure enterprise entities (User, Subscription, AuditLog)
│   ├── Application/    # DTOs, Service Interfaces, CQRS & Business Logic
│   └── Infrastructure/ # EF Core, Identity, Mailers, Payment SDKs
└── WebApi/             # Controllers, Middlewares, Program.cs setup
Enter fullscreen mode Exit fullscreen mode

Why this matters:
The Domain layer has zero dependencies on external libraries. If you want to switch from Entity Framework Core to Dapper, or swap database providers, your core business logic remains completely untouched.

  1. Abstraction Layer for Payment Billing (Stripe, Polar, Lemon Squeezy) One of the most frustrating decisions when launching a project is choosing a payment provider. Traditional processors like Stripe are great, but managing international VAT and sales tax can be a nightmare for solo builders. Merchant of Record (MoR) solutions like Polar.sh or Lemon Squeezy handle taxes automatically.

Instead of hardcoding a single vendor, I built a pluggable abstraction layer around an IBillingService interface.

public interface IBillingService
{
    Task<CheckoutSessionResponse> CreateCheckoutSessionAsync(string userId, string priceId);
    Task HandleWebhookAsync(string jsonPayload, string signatureHeader);
    Task SyncSubscriptionStatusAsync(string customerId);
}
Enter fullscreen mode Exit fullscreen mode

How it works in practice:
Developers using the template don't need to rebuild webhook infrastructure when changing providers. You simply set your preferred active provider in Program.cs / .env (e.g., Stripe, Polar, or Lemon Squeezy), and the core identity and subscription models automatically handle tier updates (Free, Pro, Enterprise).

  1. JWT Bearer Tokens + ASP.NET Core Identity & OAuth Handling authentication between a decoupled SPA (Next.js) and a backend API (.NET) can get tricky with CORS and cookie/token security.

I implemented ASP.NET Core Identity with custom JWT Bearer handling, paired with OAuth 2.0 social sign-ins (Google & GitHub).

[ Next.js Client ] ──(OAuth Redirect)──► [ Provider (Google/GitHub) ]
         │                                          │
         │ (Receive Callback Code)                   │
         ▼                                          ▼
[ Send Code to .NET API ] ───────────────► [ Validate & Issue JWT + Refresh Token ]
Enter fullscreen mode Exit fullscreen mode
  • Short-lived access tokens keep stateless API calls secure.

  • Refresh tokens stored in PostgreSQL allow seamless session renewal without forcing the user to re-login.

  1. Zero-Lock-In Containerization via Docker Compose Setting up PostgreSQL, .NET SDK, Node modules, and local webhook listeners manually on every new development machine takes hours.

I containerized the ecosystem using a single root docker-compose.yml. With one command:

docker compose up -d --build
Enter fullscreen mode Exit fullscreen mode

It spins up:

  1. Next.js Frontend (localhost:3000)

  2. .NET 8 Web API (localhost:44366 with interactive Swagger UI)

  3. PostgreSQL 16 Container (localhost:5432)

  4. Stripe CLI Container (automatically tunneling live local webhooks to the backend controller)

🛠️ Summary of Tech Stack
⚡ Backend API: ASP.NET Core 8 Web API structured with Clean Architecture (Domain, Application, Infrastructure, WebApi)

🎨 Frontend Client: Next.js (App Router), TypeScript, Tailwind CSS, and Shadcn UI / Radix primitives

🗄️ Database & ORM: PostgreSQL 16 + Entity Framework Core 8 (Npgsql) with code-first migrations

💳 Pluggable Billing: Pre-configured handlers for Stripe, Polar.sh, and Lemon Squeezy

🔐 Auth & Security: ASP.NET Core Identity, JWT Bearer tokens, and OAuth 2.0 (Google & GitHub)

🐳 DevOps & Tooling: Multi-stage Dockerfiles orchestrated via a single Docker Compose setup

Let's Discuss Architecture 🗣️
If you're building SaaS products with .NET or Next.js, I'd love to hear your thoughts:

  1. Clean Architecture vs. Vertical Slice: Do you prefer splitting code into strict layers (Domain/App/Infra), or grouping everything by feature folder?

  2. Database Choice: Do you lean towards PostgreSQL or SQL Server when deploying .NET Web APIs in production?

  3. Payment Gateways: Has anyone here transitioned from Stripe to an MoR provider like Polar or Lemon Squeezy? How was your experience?

Leave a comment below—let's chat code! 👇

Top comments (0)