AppointMe: an open-source .NET SaaS template
AppointMe is an MIT-licensed .NET 10 + React 19 SaaS template: a modular monolith with multi-tenancy, OIDC auth, permissions, CQRS, durable messaging and a one-command Aspire stack.
- What it is
- A SaaS codebase you own, MIT-licensed
- Stack
- .NET 10, React 19, SQL Server
- Modules
- Identity, Organizations, CRM, Booking
- Get it
dotnet new appointmeorgit clone
What AppointMe is
Section titled “What AppointMe is”AppointMe is the full source of a working multi-tenant SaaS. You rename it and build your product on top.
The sample product is appointment booking. Companies sign up, invite staff, keep a customer list and book appointments on a calendar, with a business dashboard over the bookings. Under that sit the parts every B2B SaaS needs and few templates finish: tenancy, sign-up and login, roles and permissions, background jobs and durable messaging.
You get all of it as code, through dotnet new or a clone. Nothing at runtime depends on a BravoDev package.
| If you are | AppointMe gives you |
|---|---|
| A .NET team starting a B2B product | Tenancy, auth and permissions already wired, with 221 tests that run without a database |
| A solo founder | One dotnet run for the whole local stack, and Bicep plus GitHub Actions for Azure |
| An agency | MIT terms for client work, and dotnet new -n to rename the solution per client |
| A developer learning the patterns | Working slices, CQRS, domain events and tenancy, each with a page that walks through the real code |
What’s inside
Section titled “What’s inside”Four modules on a shared kernel, an API host that wires them together, and a React SPA. Each row links to the page that explains it.
| Area | What you get | Read more |
|---|---|---|
| Modular monolith | Identity, Organizations, CRM and Booking, each with its own DbContext, database schema and .Contracts project |
Modular monolith |
| Vertical slices | One folder per use case, holding its command, handler, endpoint and request | Vertical slices |
| Multi-tenancy | The company comes from the X-Company-Id header and is enforced in EF Core filters, Dapper queries and messages |
Multi-tenancy |
| Auth | OIDC with JWT Bearer for API calls and cookies for the browser. Keycloak locally, Entra External ID on Azure. | Identity & principal |
| Permissions | Auto-discovered Permission fields, default grants per role, and per-company overrides |
Permissions |
| CQRS | EF Core aggregates for writes, Dapper SQL for reads | CQRS |
| Messaging | Domain events handled by Wolverine on durable queues stored in SQL Server, behind an outbox | Domain events & messaging |
| Background jobs | Hangfire recurring jobs that each module registers for itself | Background jobs |
| IDs and value objects | A strongly typed ID per aggregate, plus validated value objects such as Email and PersonName |
IDs & value objects |
| Typed frontend | TanStack Query hooks and TypeScript types generated by orval from the OpenAPI document the API build writes | Typed API client |
| Business dashboard | KPI cards, a trend chart, bookings by staff and a peak-hours heatmap | Booking module |
| Local stack | .NET Aspire starts SQL Server, Keycloak, Mailpit, the API and the SPA | Local services & ports |
| Deploy | Bicep for Azure App Service, SQL and Key Vault. GitHub Actions deploys on a v* tag. |
Azure with Bicep |
| Tests | 221 xUnit.net v3 tests in six projects | Testing |
Tech stack
Section titled “Tech stack”These are the exact pins on main (31 Oct 2026). Backend versions come from Directory.Packages.props, frontend versions from src/AppointMe.Frontend/package.json.
Backend
Section titled “Backend”| Component | Version | Notes |
|---|---|---|
| .NET SDK | 10.0.0 or later |
global.json sets rollForward: latestMajor and no previews. Projects target net10.0 with C# 14. |
| ASP.NET Core, EF Core | 10.0.14 |
All Microsoft 10.0.x packages move together |
Wolverine (WolverineFx) |
6.60.0 |
Messaging, outbox, durable local queues on SQL Server |
| Dapper | 2.1.89 |
The read side, with Dapper.SqlBuilder |
| Hangfire | 1.8.25 |
Recurring jobs, stored in SQL Server |
| .NET Aspire | 13.6.4 |
Aspire.Hosting.Keycloak is 13.6.4-preview.1.26523.11 |
| OpenTelemetry | 1.20.1 |
Exporter and ASP.NET Core, HTTP and SqlClient instrumentation |
| xUnit.net v3 | 4.0.2 |
Through xunit.v3.mtp-off, so dotnet test still runs on VSTest |
Frontend
Section titled “Frontend”| Component | Version in package.json |
Notes |
|---|---|---|
| React | ^19.3.1 |
With react-dom and react-is on the same range |
| TypeScript | ^6.0.3 |
|
Vite+ (vite-plus) |
1.4.0 |
Pinned exactly. Bundles the dev server, Oxlint and Oxfmt behind the vp command. |
| Tailwind CSS | ^4.3.0 |
With @tailwindcss/vite |
| TanStack Query | ^5.104.5 |
|
| React Router | ^8.5.0 |
|
| orval | ^8.45.0 |
Generates the API client |
| Node.js | 24 |
From .node-version. The engines field accepts ^22.18.0 || ^24.11.0 || >=26.0.0. |
Local services
Section titled “Local services”| Service | Image |
|---|---|
| SQL Server 2025 | mcr.microsoft.com/mssql/server:2025-CU10-ubuntu-24.04 |
| Keycloak | quay.io/keycloak/keycloak:26.6 |
| Mailpit | axllent/mailpit:v1.32.0 |
Free and PRO
Section titled “Free and PRO”Everything these docs describe is in the free, MIT-licensed edition. PRO is a planned one-time upgrade with billing, notifications and multi-cloud deploy, and it isn’t released yet: see Free vs PRO.
Try the live demo
Section titled “Try the live demo”The live demo signs you in to a seeded instance with one click, with no sign-up.
It’s a shared public instance, so you may see other visitors’ changes, and it may be reset from time to time. The link calls the same GET /api/v1/login/demo endpoint your local stack exposes when Demo:Enabled is on. The Quick start shows it running on your machine.
Is it a fit?
Section titled “Is it a fit?”AppointMe suits a B2B SaaS on .NET, SQL Server and Azure, where users work inside companies. The right-hand column is where it doesn’t fit.
| Good fit | Not a fit |
|---|---|
| Users belong to companies, and one user can work for several | A consumer app with no tenants. Every business record carries a CompanyId. |
| One deployable with clear module boundaries | Independent deploys per service from day one. It’s one API process and one database. |
| SQL Server locally and Azure SQL in the cloud | PostgreSQL or MySQL. EF Core, the Dapper SQL, Wolverine and Hangfire all target SQL Server. |
| Azure App Service, provisioned by Bicep | AWS or GCP today. There’s no infrastructure code for them yet. |
| A React SPA talking to a typed API | A Blazor or Razor Pages UI |
| Logical tenant isolation in one shared database | A database per tenant. The seam exists, the code doesn’t: see Multi-tenancy. |
| Owning the code and merging upstream changes when you choose | Getting framework upgrades from a package bump. Updates arrive as Git changes you merge. |
| A product that can launch without payments | Billing on day one. There’s no payments domain, and the dashboard’s revenue figures are placeholder data. |
Why a template you copy, not a framework you reference?1 min · optional
A framework package keeps its code in a DLL you don’t control, and its upgrades land on its schedule. AppointMe hands you the source instead. dotnet new appointme writes the whole solution into your repository under your names, and you can change any line of it.
The trade-off is updates. A fix upstream reaches you as a change you merge, not a version bump. Take updates from upstream covers how.