Skip to content
StarLive demo →
OverviewChecked against main · 31 Oct 20266 min read

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 appointme or git clone

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

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

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.

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
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.
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

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.

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.

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.