AppointMe vs FullStackHero
AppointMe and FullStackHero are both MIT .NET 10 modular-monolith SaaS templates. FullStackHero ships more modules and tests; AppointMe adds external OIDC, Wolverine messaging and multi-company users.
- Both are
- MIT, .NET 10, modular monolith, React 19
- FSH leads on
- Module count, tests, per-tenant databases
- AppointMe leads on
- External OIDC, durable messaging, multi-company users
- Pick FSH if
- You want the most built-in modules today
At a glance
Section titled “At a glance”Both start from the same shape, a .NET 10 modular monolith with vertical slices, and differ most in identity, messaging and breadth.
| AppointMe | FullStackHero | |
|---|---|---|
| Architecture | Modular monolith, vertical slices. 4 modules (Identity, Organizations, CRM, Booking), each with its own DbContext, schema and .Contracts project. |
Modular monolith, vertical slices. 10 modules (Identity, Multitenancy, Auditing, Files, Chat, Notifications, Webhooks, Billing, Catalog, Tickets) on shared BuildingBlocks, each with a .Contracts project. |
| Multi-tenancy | One shared database with a CompanyId on every row. The company comes from the X-Company-Id header on each request, and one user can belong to several companies. |
Finbuckle.MultiTenant 10. A signed-in caller’s tenant comes from the tenant claim in its JWT. Shared database by default, with an optional connection string per tenant. |
| Auth | OIDC with Keycloak locally and Entra External ID on Azure. JWT Bearer for the API, cookies for browser flows, sign-up through the app. | ASP.NET Core Identity issuing its own JWTs with rotating refresh tokens. TOTP two-factor, sessions, password policies and impersonation. |
| Permissions | Permission fields auto-discovered from code, 4 built-in roles, per-company role overrides. Checked in the handler with principal.Require(...). |
A flat permission registry, checked with .RequirePermission(...) on endpoints. Roles and user groups; permissions resolved server-side. |
| Data access | EF Core 10 for writes, Dapper for reads. SQL Server. | EF Core 10 with the specification pattern and soft-delete and audit interceptors. PostgreSQL. |
| Messaging | Wolverine 6: domain events and handlers on durable local queues in SQL Server. Azure Service Bus by config. | Its own outbox, inbox and dispatcher. In-memory bus by default, RabbitMQ as an option. |
| Frontend | One React 19 SPA: TypeScript 6, Vite+, Tailwind 4, shadcn/ui, TanStack Query. Hooks and types generated by orval. | Two React 19 apps, an operator console and a tenant app: Vite 8, TypeScript 6, TanStack Query, Radix and Tailwind 4. Hand-written typed client, SignalR and SSE. |
| Local dev | Aspire runs SQL Server, Keycloak, Mailpit, the API and the SPA, then migrates and seeds demo data. compose.yaml as an alternative. |
Aspire runs Postgres, Valkey, RustFS (S3), a migrator, a demo seeder, the API and both apps. An fsh CLI scaffolds and checks your setup. |
| Deploy | Bicep for Azure App Service, Azure SQL and Key Vault. GitHub Actions with OIDC deploys on a v* tag. |
Docker Compose production stack, Terraform for AWS (ECS Fargate, RDS, ElastiCache, S3 and CloudFront), and Dokploy. |
| Tests | 221 xUnit unit tests, with no database or HTTP host. No architecture, integration or frontend tests. | 1,600+ backend tests, including NetArchTest architecture tests and Testcontainers integration tests, plus 200+ Playwright tests, per its README. |
| Docs | This site, about 50 pages. | 93 pages at fullstackhero.net/docs. |
| License | MIT. A PRO add-on is planned. | MIT. No paid tier. |
| Maintainers and community | One main maintainer (BravoDev). About 50 GitHub stars. Public since June 2026. | Led by Mukesh Murugan, with more than 50 contributors. About 6.9k stars. Public since 2021, sponsored through Open Collective. |
| Live demo | One click, no sign-up. | None hosted. You run it locally with seeded accounts. |
AppointMe facts are from commit 2ccbe87. FullStackHero facts are from its main branch, README and docs (see Sources).
Where AppointMe is stronger
Section titled “Where AppointMe is stronger”AppointMe goes deeper on three foundations: who the user is, which company they act for, and how messages survive a restart.
- Identity lives in an identity provider. Users sign in through OIDC: Keycloak locally, Entra External ID on Azure. Password storage, reset and email verification happen in the provider, and the app keeps its own
Userrecord. See Identity & principal. - One user, many companies. The company is picked per request from
X-Company-Id, and access is checked against the user’s employee record for that company. A receptionist who works for two clinics switches between them without signing in again. See Multi-tenancy. - Messaging on a maintained library. Wolverine publishes domain events from EF Core and runs handlers on durable queues stored in SQL Server, and the company and caller travel on the message envelope. FullStackHero has an outbox too, but its dispatcher and bus are code you own and maintain. See Domain events & messaging.
- Hand-written SQL for reads. Commands go through EF Core aggregates, while lists, search and the dashboard are Dapper queries you can read and tune. See CQRS.
- A generated frontend client. The API build writes its OpenAPI document, and orval turns it into TypeScript types and TanStack Query hooks, so a changed endpoint becomes a compile error in the SPA. See Typed API client.
- A smaller core and a public demo. Four modules, two of which (CRM and Booking) are examples you can remove. The live demo logs you in with one click.
- An Azure path. Bicep, Key Vault references, a managed identity and secret-less GitHub OIDC are wired up. See Azure with Bicep.
Where FullStackHero is stronger
Section titled “Where FullStackHero is stronger”FullStackHero is broader, better tested and more established, and those gaps are real.
- More modules, ready now. Auditing, files on S3 with presigned URLs, signed webhooks, chat, a notification inbox, tickets and a catalog. Its Billing module tracks plans, invoices and usage, but doesn’t take payments: its docs say “no Stripe SDK, no card webhooks”. AppointMe’s billing and notifications are planned for PRO.
- A much larger test suite. Architecture tests enforce module boundaries, Testcontainers runs integration tests against real PostgreSQL, and Playwright covers both apps. AppointMe has unit tests only, and its module boundaries rely on project references. See Testing.
- More tenancy options. Each tenant can have its own connection string. Creating a tenant runs a provisioning job (database, migrations, seeding, cache warm-up) that resumes from the failed step. AppointMe supports one shared database.
- Account features inside the app. TOTP two-factor, session management and time-limited impersonation across tenants. AppointMe has no impersonation, and anything like 2FA is whatever you configure in Keycloak or Entra.
- An operator console. A separate admin app for whoever runs the platform. In AppointMe, super admins (the
Authentication:SuperAdminsemail list) only get the Hangfire dashboard. - AWS and production plumbing. Terraform for AWS, a Docker Compose production stack, and a one-shot DbMigrator so the API never migrates on startup. AppointMe applies migrations when the API starts and ships Bicep for Azure only.
- Track record. Public since 2021, about 6.9k stars, more than 50 contributors and 93 docs pages. AppointMe went public in June 2026 and has one main maintainer, so its bus factor is lower.
Why an external identity provider instead of ASP.NET Core Identity?2 min · optional
Issuing your own tokens means you own password hashing, lockout, reset flows, two-factor and signing-key rotation. Keycloak and Entra External ID already do that work. AppointMe only validates their tokens and keeps a local User row for its own data.
The cost is one more moving part. Keycloak runs as a container in local dev, and production needs an Entra External ID tenant created in the portal. Sign-up and invitations go through the app’s IIdentityProvider, so changing provider touches one interface.
If you want users, tokens and data in one database and one deployable, FullStackHero’s in-app Identity is simpler to run.
Choose FullStackHero if… / choose AppointMe if…
Section titled “Choose FullStackHero if… / choose AppointMe if…”Decide on the identity model and on breadth: FullStackHero for the most in-app features, AppointMe for an external IdP and a smaller core.
| Choose FullStackHero if… | Choose AppointMe if… |
|---|---|
| You want auditing, files, webhooks or chat on day one | You’d rather add modules to a smaller core you understand end to end |
| You want users, passwords and tokens in your own database | You want sign-in handled by Keycloak, Entra External ID or another OIDC provider |
| Some tenants need their own database | One shared database with a CompanyId column fits your scale |
| Each user belongs to one tenant | Users work for several companies and switch between them |
| You deploy to AWS or a single Docker host | You deploy to Azure |
| You run PostgreSQL | You run SQL Server |
| A large test suite and an established community matter most | You want durable messaging and Dapper reads already wired |
Migrating from FullStackHero
Section titled “Migrating from FullStackHero”There’s no migration tool: you port each slice onto AppointMe’s equivalent, and the biggest step is moving users to the identity provider.
| FullStackHero | AppointMe | Notes |
|---|---|---|
Tenant (AppTenantInfo, Finbuckle) |
Company in the Organizations module, keyed by CompanyId |
One shared database. There are no per-tenant connection strings. |
tenant claim or header |
X-Company-Id header, checked against the user’s employee record |
The SPA sends the header on every call. |
IGlobalEntity (not tenant-scoped) |
An entity with no CompanyId filter, such as User |
|
| Global EF tenant filter | A named filter per entity, plus [CompanyId] = @CompanyId in every Dapper read |
See writes and reads. |
Module and .Contracts project |
Module and .Contracts project |
Same shape. Other modules reference only .Contracts. |
| Mediator command and handler | A Wolverine handler with a HandleAsync method |
Add CompanyId and IPrincipal parameters to get the tenant and the caller. |
| FluentValidation validator | Value-object factories such as Email.CreateOrNull |
They throw ValidationException. See IDs & value objects. |
.RequirePermission(...) on the endpoint |
principal.Require(CustomerPermissions.View) in the handler |
See Add a permission. |
| Integration event through the outbox | An event record in the publisher’s .Contracts project, handled on a durable queue |
See Cross-module events. |
| ASP.NET Core Identity users | Users in Keycloak or Entra External ID, plus a local User row |
Plan how existing users set a new password. |
| PostgreSQL | SQL Server | Regenerate migrations. Raw SQL uses [brackets]. |
Sources
Section titled “Sources”Every FullStackHero fact on this page comes from the project’s own repository and docs, accessed Nov 2026.
| Source | Used for |
|---|---|
| github.com/fullstackhero/dotnet-starter-kit | README (modules, stack, test counts, deploy), repository tree, src/Directory.Packages.props, stars and contributors |
| fullstackhero.net | No paid tier, Open Collective sponsorship, no hosted demo |
| fullstackhero.net/docs/modules/multitenancy/ | Tenant resolution, isolation model, provisioning |
| fullstackhero.net/docs/modules/identity/ | JWT and refresh tokens, permissions, two-factor, impersonation |
| fullstackhero.net/docs/modules/billing/ | Billing scope and “no Stripe SDK” |
| fullstackhero.net/docs/building-blocks/eventing/ | Outbox, inbox and bus providers |
| fullstackhero.net/sitemap-0.xml | Docs page count |
AppointMe facts are from bravodev-hub/appointme at commit 2ccbe87.