Skip to main content

Prerequisites

1

Install .NET 10 SDK

Download from dotnet.microsoft.comVerify installation:
dotnet --version
# Output: 10.0.x
2

Install Aspire Workload

dotnet workload install aspire
Aspire orchestrates Postgres, Redis, and the API with automatic service discovery and OpenTelemetry.
3

Install Docker

Aspire uses Docker to run Postgres and Redis containers.Verify Docker is running:
docker ps

Run the API (Aspire)

From src/README.md:27-33:
1

Restore Dependencies

cd src
dotnet restore FSH.Framework.slnx
This restores NuGet packages for all projects in the solution.
2

Start Everything with Aspire

dotnet run --project Playground/FSH.Playground.AppHost
Aspire brings up:
Expected output:
info: Aspire.Hosting.DistributedApplication[0]
  Aspire version: 10.0.0
info: Aspire.Hosting.DistributedApplication[0]
  Distributed application starting.
info: Aspire.Hosting.DistributedApplication[0]
  Application host directory is: /src/Playground/FSH.Playground.AppHost
info: Aspire.Hosting.DistributedApplication[0]
  Now listening on: http://localhost:15888
info: Aspire.Hosting.DistributedApplication[0]
  Login to the dashboard at http://localhost:15888
3

Verify API is Running

Open your browser to https://localhost:5285You should see the Scalar API documentation interface.Or use curl:
curl https://localhost:5285 -k
# Output: {"message":"hello world!"}
From src/Playground/Playground.Api/Program.cs:66-68:
app.MapGet("/", () => Results.Ok(new { message = "hello world!" }))
   .WithTags("PlayGround")
   .AllowAnonymous();

Explore the API

Access Swagger/Scalar UI

Navigate to https://localhost:5285/scalar/v1 in your browser.
FullStackHero uses Scalar instead of Swagger UI for a modern, interactive API documentation experience.
From src/Playground/Playground.Api/appsettings.json:89-103:
Configuration
"OpenApiOptions": {
  "Enabled": true,
  "Title": "FSH PlayGround API",
  "Version": "v1",
  "Description": "The FSH Starter Kit API for Modular/Multitenant Architecture.",
  "Contact": {
    "Name": "Mukesh Murugan",
    "Url": "https://codewithmukesh.com",
    "Email": "[email protected]"
  },
  "License": {
    "Name": "MIT License",
    "Url": "https://opensource.org/licenses/MIT"
  }
}

Available Modules

Endpoints under /api/v1/identity:
  • POST /api/v1/identity/token/issue - Generate JWT tokens
  • POST /api/v1/identity/token/refresh - Refresh expired tokens
  • GET /api/v1/identity/users/search - Search users (requires auth)
  • POST /api/v1/identity/users/register - Register new user
  • GET /api/v1/identity/roles - List roles
  • GET /api/v1/identity/groups - List user groups
From src/Modules/Identity/Modules.Identity/Features/v1/

Your First API Call

1. Generate a JWT Token

From src/BuildingBlocks/Shared/Multitenancy/MultitenancyConstants.cs:9 and :14, the default admin credentials are:
curl -X POST https://localhost:5285/api/v1/identity/token/issue \
  -H "Content-Type: application/json" \
  -H "tenant: root" \
  -d '{
    "email": "[email protected]",
    "password": "123Pa$$word!"
  }' \
  -k
Expected Response:
{
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "accessTokenExpiresAt": "2026-03-06T23:30:00Z",
  "refreshToken": "CfDJ8K...",
  "refreshTokenExpiresAt": "2026-03-13T21:30:00Z"
}
From src/Modules/Identity/Modules.Identity/Features/v1/Tokens/TokenGeneration/GenerateTokenEndpoint.cs:20-38:
From src/Modules/Identity/Modules.Identity/Features/v1/Tokens/TokenGeneration/GenerateTokenCommandHandler.cs:60-92:
  1. Validate credentials against ASP.NET Identity
  2. Audit login attempt (success or failure)
  3. Issue JWT with user claims (roles, permissions, tenant)
  4. Store refresh token (hashed) for later use
  5. Create user session for session management
  6. Audit token issuance with fingerprint
  7. Enqueue integration event for downstream systems
All of this happens in GenerateTokenCommandHandler.Handle().

2. Call an Authenticated Endpoint

Use the accessToken from step 1:
curl -X GET https://localhost:5285/api/v1/identity/users/search \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "tenant: root" \
  -k
Expected Response:
{
  "data": [
    {
      "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "email": "[email protected]",
      "firstName": "Root",
      "lastName": "Admin",
      "isActive": true,
      "emailConfirmed": true,
      "roles": ["Admin"]
    }
  ],
  "currentPage": 1,
  "totalPages": 1,
  "totalCount": 1,
  "pageSize": 10
}
From src/Modules/Identity/Modules.Identity/Features/v1/Users/SearchUsers/SearchUsersEndpoint.cs:15-23:
How authorization works
return endpoints.MapGet(
        "/users/search",
        async ([AsParameters] SearchUsersQuery query,
               IMediator mediator,
               CancellationToken cancellationToken) =>
            await mediator.Send(query, cancellationToken))
    .WithName("SearchUsers")
    .WithSummary("Search users with pagination")
    .RequirePermission(IdentityPermissionConstants.Users.View);  // <-- Requires permission
Every protected endpoint uses .RequirePermission() for fine-grained authorization. This is enforced via a custom authorization filter.

3. Check API Health

curl https://localhost:5285/health -k
Expected Response:
{
  "status": "Healthy",
  "checks": [
    {
      "name": "self",
      "status": "Healthy",
      "description": null,
      "data": {}
    }
  ],
  "totalDuration": "00:00:00.0012345"
}
From src/BuildingBlocks/Web/Extensions.cs:63:
Health check registration
builder.Services.AddHealthChecks()
    .AddCheck("self", () => HealthCheckResult.Healthy());

Understanding the Configuration

From src/Playground/Playground.Api/appsettings.json:
Line 66-70
"DatabaseOptions": {
  "Provider": "POSTGRESQL",
  "ConnectionString": "Server=localhost;Database=fsh;User Id=postgres;Password=password",
  "MigrationsAssembly": "FSH.Playground.Migrations.PostgreSQL"
}
When using Aspire, the connection string is injected automatically from the Postgres container.From src/Playground/FSH.Playground.AppHost/AppHost.cs:4 and :14-15:
var postgres = builder.AddPostgres("postgres")
    .WithDataVolume("fsh-postgres-data")
    .AddDatabase("fsh");

builder.AddProject<Projects.Playground_Api>("playground-api")
    .WithReference(postgres)  // Auto-injects connection string
    .WithEnvironment("DatabaseOptions__Provider", "POSTGRESQL")
    .WithEnvironment("DatabaseOptions__ConnectionString",
                    postgres.Resource.ConnectionStringExpression)

Run Without Aspire (API Only)

If you want to run just the API without Aspire orchestration:
1

Start Postgres and Redis

docker run -d --name fsh-postgres \
  -e POSTGRES_PASSWORD=password \
  -e POSTGRES_DB=fsh \
  -p 5432:5432 \
  postgres:16

docker run -d --name fsh-redis \
  -p 6379:6379 \
  redis:7-alpine
2

Update appsettings.Development.json

Ensure connection strings point to your local containers:
{
  "DatabaseOptions": {
    "ConnectionString": "Server=localhost;Database=fsh;User Id=postgres;Password=password"
  },
  "CachingOptions": {
    "Redis": "localhost:6379"
  }
}
3

Run the API

dotnet run --project src/Playground/Playground.Api
From src/Playground/Playground.Api/Program.cs:59 and :38, the host:
  • Applies migrations via UseHeroMultiTenantDatabases()
  • Maps module endpoints via UseHeroPlatform(p => p.MapModules = true)
app.UseHeroMultiTenantDatabases();
app.UseHeroPlatform(p =>
{
    p.MapModules = true;
    p.ServeStaticFiles = true;
});

Access Background Jobs Dashboard

Hangfire dashboard is available at https://localhost:5285/jobs From src/Playground/Playground.Api/appsettings.json:77-81:
"HangfireOptions": {
  "Username": "admin",
  "Password": "Secure1234!Me",
  "Route": "/jobs"
}
Change the default credentials in production via environment variables.

Next Steps

Add Your First Feature

Learn the vertical slice pattern: Command → Handler → Validator → Endpoint

Understand Modules

Explore Identity, Multitenancy, and Auditing modules

Configure Multi-Tenancy

Set up tenant isolation and provisioning

Deploy to Production

Deploy to Azure, AWS, or Kubernetes

Common Issues

Stop conflicting containers:
docker stop $(docker ps -q --filter "publish=5432")
docker stop $(docker ps -q --filter "publish=6379")
Or change ports in AppHost.cs:
var postgres = builder.AddPostgres("postgres")
    .WithEndpoint(port: 5433)  // Custom port
    .AddDatabase("fsh");
Use -k flag with curl or set DOTNET_SYSTEM_NET_HTTP_SOCKETSHTTPHANDLER_ALLOWALL=1For production, configure proper SSL certificates:
dotnet dev-certs https --trust
Manually apply migrations:
dotnet ef database update --project src/Playground/Migrations.PostgreSQL
Or set MultitenancyOptions:RunTenantMigrationsOnStartup: true in appsettings.
  1. Verify you included the Authorization: Bearer <token> header
  2. Check token hasn’t expired (default: 2 minutes for access tokens)
  3. Ensure you’re passing the correct tenant header
  4. Confirm user has the required permission (check role assignments)

What Just Happened?

From src/Playground/Playground.Api/Program.cs:30-56, when you ran the API:
1

Registered Mediator

All command/query handlers from Identity, Multitenancy, and Auditing modules
Line 30-40
builder.Services.AddMediator(o =>
{
    o.ServiceLifetime = ServiceLifetime.Scoped;
    o.Assemblies = [
        typeof(GenerateTokenCommand),
        typeof(GenerateTokenCommandHandler),
        typeof(GetTenantStatusQuery),
        typeof(GetTenantStatusQueryHandler),
        typeof(FSH.Modules.Auditing.Contracts.AuditEnvelope),
        typeof(FSH.Modules.Auditing.Persistence.AuditDbContext)];
});
2

Registered Hero Platform

Caching, Mailing, Jobs, OpenTelemetry, Health Checks, Authentication, Authorization, Rate Limiting
Line 49-54
builder.AddHeroPlatform(o =>
{
    o.EnableCaching = true;
    o.EnableMailing = true;
    o.EnableJobs = true;
});
From src/BuildingBlocks/Web/Extensions.cs:29-87, this wires 11 building blocks.
3

Registered Modules

Identity, Multitenancy, and Auditing modules with their DbContexts and endpoints
Line 42-56
var moduleAssemblies = new Assembly[]
{
    typeof(IdentityModule).Assembly,
    typeof(MultitenancyModule).Assembly,
    typeof(AuditingModule).Assembly
};

builder.AddModules(moduleAssemblies);
4

Applied Tenant Migrations

EF Core migrations for all active tenants
Line 59
app.UseHeroMultiTenantDatabases();
5

Mapped Module Endpoints

All endpoints from Identity, Multitenancy, and Auditing modules under /api/v1/
Line 60-64
app.UseHeroPlatform(p =>
{
    p.MapModules = true;
    p.ServeStaticFiles = true;
});

Ready to Build?

Learn how to add your first feature using the vertical slice pattern

Build docs developers (and LLMs) love