Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/ti-infinite/GSMApplication/llms.txt

Use this file to discover all available pages before exploring further.

GSM Application uses a strict database-per-tenant architecture: every company that subscribes to the platform gets its own dedicated SQL Server database. No tenant shares tables, schemas, or connection pools with another. The central TenantRegistryDb database acts as a routing directory — it maps a company identifier to the precise server and database that holds that company’s data.

The Tenants Registry Table

All tenant routing metadata lives in TenantRegistryDb.Tenants. The table is created by the following DDL:
CREATE DATABASE TenantRegistryDb;
GO
USE TenantRegistryDb;
GO

CREATE TABLE Tenants (
    CompanyId  NVARCHAR(50)  NOT NULL PRIMARY KEY,
    Server     NVARCHAR(255) NOT NULL,
    Database   NVARCHAR(255) NOT NULL,
    DbUser     NVARCHAR(255) NOT NULL,
    DbPassword NVARCHAR(255) NOT NULL,
    IsActive   BIT           NOT NULL DEFAULT(1)
);
CompanyId
NVARCHAR(50)
required
Primary key. Short alphanumeric identifier for the company — e.g. IH001. This value is embedded in every JWT as the companyId claim.
Server
NVARCHAR(255)
required
Hostname or instance name of the SQL Server that hosts the tenant database.
Database
NVARCHAR(255)
required
Name of the tenant-specific database on the target server.
DbUser
NVARCHAR(255)
required
SQL login username used when building the dynamic connection string.
DbPassword
NVARCHAR(255)
required
Password for the SQL login. Store this value in an encrypted column or a secrets manager in production environments.
IsActive
BIT
required
Controls whether the tenant is operational. Only rows where IsActive = 1 are returned by TenantConnectionResolver.
Tenants with IsActive = 0 cannot authenticate. The TenantConnectionResolver filters exclusively on active records, so a deactivated tenant returns null from the resolver, which causes the login endpoint to return 401 Unauthorized (with an “invalid credentials” message) before any password check occurs.

Runtime Tenant Resolution

Every authenticated API request travels through the following resolution chain before any microservice reads from a database.
1

Client sends a Bearer token

The browser or API client attaches Authorization: Bearer <jwt> to the outbound HTTP request and sends it to the GSM Gateway.
2

Gateway validates the JWT

The gateway’s JWT middleware validates the signature, issuer, and audience of the token. If validation fails the gateway returns 401 Unauthorized immediately and the request never reaches a microservice.
3

Tenant header injection

TenantHeaderInjectionMiddleware runs after JWT validation. It performs two operations atomically:
  1. Strips any X-Company-Id or X-Profile-Id headers that the client may have sent — clients must never be trusted to supply their own tenant identity.
  2. Injects a fresh X-Company-Id header derived from the JWT’s companyId claim, and an X-Profile-Id header derived from the idProfile claim.
// TenantHeaderInjectionMiddleware.cs
context.Request.Headers.Remove(TenantHeaderConstants.CompanyIdHeaderName); // "X-Company-Id"
context.Request.Headers.Remove(TenantHeaderConstants.ProfileIdHeaderName); // "X-Profile-Id"

var companyId = gatewayTenantService.ResolveCompanyId(context.User);

if (!string.IsNullOrWhiteSpace(companyId))
{
    tenantContext.CompanyId = companyId;
    context.Request.Headers[TenantHeaderConstants.CompanyIdHeaderName] = companyId;
}

var idProfile = context.User.FindFirst("idProfile")?.Value;
if (!string.IsNullOrWhiteSpace(idProfile))
    context.Request.Headers[TenantHeaderConstants.ProfileIdHeaderName] = idProfile;
4

YARP forwards the enriched request

The reverse proxy routes the request — now carrying the trusted X-Company-Id header — to the appropriate downstream microservice cluster.
5

Microservice reads the tenant header

Each microservice’s TenantMiddleware reads X-Company-Id from the incoming request and stores it in its local TenantContext for the duration of that request scope.
6

Dynamic DbContext is constructed

The microservice’s TenantConnectionResolver queries TenantRegistryDb.Tenants using the CompanyId value, builds a connection string from the returned Server, Database, DbUser, and DbPassword columns, and instantiates an EF Core DbContext bound to that specific tenant database.
7

Query executes against the tenant database

All subsequent EF Core queries in the request handler operate entirely within the isolated tenant database.

Fallback for Direct Service Calls

In development it is sometimes useful to call a microservice directly without routing through the gateway. For these scenarios each microservice’s TenantExtensions logic attempts to read X-Company-Id first and, if the header is absent, falls back to the companyId JWT claim embedded in the bearer token.
The JWT-claim fallback is intended only for local development and integration testing. In production all traffic passes through the gateway, which always supplies the verified header.

TenantContext and TenantMiddleware Pattern

Every microservice in the GSM ecosystem replicates the same lightweight tenant-awareness pattern:

TenantContext

A scoped service that holds the resolved CompanyId string for the current HTTP request. Business logic, repositories, and DbContext factories inject this object to know which tenant they are serving.

TenantMiddleware

Registered early in the middleware pipeline. Reads X-Company-Id (or falls back to the JWT claim) and populates TenantContext. Downstream handlers never parse the header themselves.

Adding a New Tenant

1

Create the tenant database

On the target SQL Server, create a new database following the naming convention — for example GSM_{CompanyId}_Db.
CREATE DATABASE GSM_NEWCO_Db;
2

Run the schema scripts

Apply all tenant schema migrations to the new database. Each microservice ships its own schema script for its domain tables (Users, Employees, Suppliers, etc.).
3

Insert the registry row

Add a record to TenantRegistryDb.Tenants. Set IsActive = 1 only when the database and schema are fully ready.
INSERT INTO TenantRegistryDb.dbo.Tenants
    (CompanyId, Server,         Database,       DbUser,   DbPassword,     IsActive)
VALUES
    ('NEWCO',   'sql-server-01', 'GSM_NEWCO_Db', 'sa-app', 'str0ngP@ss',  1);
4

Seed initial data

Insert the default users, profiles, menu configuration, and any tenant-specific parameters required for the new company to function.
5

Set JsonStyles (optional)

Populate the JsonStyles column if the tenant requires custom branding. See Tenant Theming for the full JSON structure.
INSERT INTO Tenants (CompanyId, Server, Database, DbUser, DbPassword, IsActive)
VALUES ('GSM001', '(localdb)\mssqllocaldb', 'Auth_GSM001_Db', 'sa-demo', 'demo-password', 1);
This matches the seed row shipped in tenant-registry.sql. Use it to bootstrap a local development environment against SQL Server LocalDB.

Build docs developers (and LLMs) love