In the project, the sub-schema strategy is employed to implement a multi-tenant architecture. Here's how it works:
Sub-schema Strategy:
This strategy involves creating separate database schemas for each tenant, rather than separate databases. Each schema acts as a self-contained unit within the same database instance, providing isolation and security between tenants' data.
+----------------------+
| Public Schema DB |
| |
| +--------------+ |
| | Public | |
| | Tables | |
| | | |
| +--------------+ |
+----------------------+
|
|
V
+----------------------+
| Sub-Schema DBs |
| |
| +--------------+ |
| | Tenant 1 | |
| | Tables | |
| | | |
| +--------------+ |
| |
| +--------------+ |
| | Tenant 2 | |
| | Tables | |
| | | |
| +--------------+ |
| |
| ... |
+----------------------+
This repository organizes the application into functional modules, distinguishing between public and tenanted functionalities for clarity.
- Public Module: Contains the public tenants table.
- Tenanted Module: Holds all tenanted functionalities, such as users in this project.
βββ πmulti-tenant-task
βββ .env
βββ Dockerfile
βββ docker-compose.yaml
βββ package-lock.json
βββ package.json
βββ πsrc
βββ abstract.entity.ts
βββ app.module.ts
βββ main.ts
βββ πmigrations
βββ πpublic
βββ 1638963391898-AddTenants.ts
βββ πtenanted
βββ 1638963474130-AddUsers.ts
βββ πmodules
βββ πpublic
βββ πtenants
βββ πdto
βββ create-tenant.dto.ts
βββ tenant.entity.ts
βββ tenants.module.ts
βββ tenants.resolvers.ts
βββ tenants.service.ts
βββ πtenancy
βββ tenancy.middleware.ts
βββ tenancy.module.ts
βββ tenancy.symbols.ts
βββ tenancy.utils.ts
βββ πtenanted
βββ πauth
βββ auth.module.ts
βββ auth.resolvers.ts
βββ auth.service.ts
βββ πdto
βββ access-token.dto.ts
βββ jwt.dto.ts
βββ login.input.ts
βββ refresh-token.args.ts
βββ singup.input.ts
βββ πguards
βββ authorize.guard.ts
βββ gql-auth.guard.ts
βββ role.guard.ts
βββ password.service.ts
βββ role.enum.ts
βββ πstrategies
βββ jwt.strategy.ts
βββ πusers
βββ πdto
βββ create-user.dto.ts
βββ user.entity.ts
βββ users.module.ts
βββ users.resolvers.ts
βββ users.service.ts
βββ public.orm.config.ts
βββ schema.gql
βββ seed.ts
βββ tenants.orm.config.ts
βββ tsconfig.build.json
βββ tsconfig.json
There are two TypeORM configuration files:
public.orm.config.ts
: Configuration for public entities.tenants.orm.config.ts
: Configuration for tenanted entities.
- Public Migrations: Standard TypeORM migration setup for the public schema.
- Tenants Migrations: Manually written migrations for tenanted schemas, ensuring tables are prefixed with their schema name and constraint keys are uniquely labeled.
Requests are handled using an x-tenant-id
header property sent with each request. The tenancy module middleware extracts this header and attaches it to the request. Each request is scoped to obtain the correct connection, ensuring operations are performed on the correct schema based on the provided tenant ID.
- Authorize Guard: Utilizes password JWT to authenticate requests and verify the correct tenant ID.
Upon application startup:
- Public migrations are executed.
- For each tenant, a schema is created in the database.
- When an authenticated user creates a tenant:
- The tenant service creates the tenant.
- A new schema is created in the database for the tenant.
- Migrations are run in this schema.
- The connection to this schema is closed.
- The user is added to the tenants users with admin role, enabling them to create/invite users to their tenant.
- I added just one e2e test as POC.
docker compose up
npm install --legacy-peer-deps
# e2e tests
$ npm run test:e2e