Archticture:

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

Request Journey: image

Repository Structure

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.

Folder Structure

└── πŸ“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

ORM Configuration

There are two TypeORM configuration files:

  • public.orm.config.ts: Configuration for public entities.
  • tenants.orm.config.ts: Configuration for tenanted entities.

Migrations

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

Handling Requests

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.

Security

  • Authorize Guard: Utilizes password JWT to authenticate requests and verify the correct tenant ID.

Application Workflow

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.

Testing

  • I added just one e2e test as POC.

Running the app

docker compose up 

Test

npm install --legacy-peer-deps
# e2e tests
$ npm run test:e2e