Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 7

Tenant Management & Authentication Services: Low-Level Design

Recommended resources

Welcome back. In our previous session, we architected a flexible and secure Role-Based Access Control (RBAC) system using a hybrid model of role templates and tenant-specific custom roles. We established the "what" and the "who" of our security policy. Now, it's time to design the "how"—the actual entry points into our system.

Today's objective is to move from architectural theory to a concrete low-level design for our tenant management and authentication services. We will specify the precise API endpoints and data flows that will allow a new company to register on our platform and its users to authenticate securely. This is the front door to our SaaS, and getting its design right is critical for security, scalability, and developer experience.

The "Account-First" Principle: A SaaS Cornerstone

Before we write a single endpoint definition, we must internalize the most critical principle of multi-tenant application design: you register an account (tenant), not just a user. Your experience with Django's user model is a good starting point, but in a SaaS context, the user is subordinate to the tenant. The tenant owns the data; the user is merely an actor within that tenant's boundary.

This "account-first" or "tenant-first" approach ensures that from the moment of creation, every piece of data has a clear owner. The first user created during registration is special—they become the Company Admin we defined in our RBAC design.

Multi-Tenant SaaS Architecture: Getting Account Creation Right ...

This article from dev.to brilliantly articulates the "account-first" philosophy. It's a pragmatic guide that aligns perfectly with our goal of building a robust system from day one.

Please read the first three key sections: The Account-First Decision, which establishes the core principle. Then, study Transaction-Based Consistency to understand the importance of atomicity in this process. Finally, review the Admin Bootstrap Strategy, which details how the first user becomes the tenant's administrator.

As you've just read, the registration process is not a single INSERT statement. It's a transaction that must succeed or fail as a whole, creating the Tenant (company), the initial User, their role as Company Admin, and their default Subscription status all at once. This prevents orphaned records and ensures a consistent state.


Low-Level Design: Tenant & Auth Services

With the account-first principle as our guide, let's define the API contracts. We will group these endpoints under an /auth path for clarity.

1. Company & Admin Registration

This is the primary public endpoint for onboarding a new company.

  • Endpoint: POST /api/v1/auth/register

  • Description: Creates a new tenant (company) and its initial administrator user.

  • Request Body:

    {
      "company_name": "Innovate Construction Inc.",
      "email": "admin@innovateconstruction.com",
      "password": "a-very-strong-password"
    }
    
  • Response (Success - 201 Created):

    {
      "message": "Company and admin user created successfully."
    }
    

    Architect's Note: We will deliberately not return tokens here. Registration and login are separate concerns. After registering, the user should be redirected to a login page to perform their first authentication. This is a cleaner, more secure flow.

  • Data Flow:

    1. Validation: The service layer receives the request payload. It first checks for global uniqueness constraints:
      • Does a user with this email already exist in the users table?
      • Does a company with this company_name already exist in the tenants table? If so, reject the request (409 Conflict).
    2. Transaction Start: Begin a database transaction.
    3. Create Tenant: Create a new record in the tenants table.
    4. Create Subscription: Create a corresponding record in the subscriptions table, linked to the new tenant, with a default plan (e.g., "trial").
    5. Hash Password: Securely hash the user's provided password using a strong algorithm like Argon2 or bcrypt.
    6. Create User: Create a new record in the users table, including the hashed password and linking it to the new tenant_id.
    7. Assign Role: In the user_roles junction table, assign this new user the system's default Company Admin role.
    8. Transaction Commit: If all steps succeed, commit the transaction. If any step fails, roll back the entire transaction.
    9. (Optional Async Task): Dispatch a background task to send a "Welcome" email.
    10. Return Response: Send the 201 Created response to the client.

2. User Authentication (Token Generation)

This endpoint exchanges user credentials for access and refresh tokens.

  • Endpoint: POST /api/v1/auth/token

  • Description: Authenticates a user and returns a JWT token pair. This follows the OAuth 2.0 convention for token endpoints.

  • Request Body: application/x-www-form-urlencoded

    username=admin@innovateconstruction.com&password=a-very-strong-password
    

    Architect's Note: Using form data for the token endpoint is a best practice from the OAuth 2.0 specification (RFC 6749). While FastAPI can easily handle JSON, adhering to this standard improves interoperability.

  • Response (Success - 200 OK):

    {
      "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
      "refresh_token": "def502002733a17e57...",
      "token_type": "bearer"
    }
    
  • JWT Payload Design: The access token is not just an opaque string; it's a self-contained credential. Its payload is critical for our stateless architecture.

    {
      "sub": "user_uuid_12345", // Subject (User ID)
      "tenant_id": "tenant_uuid_abcde", // Tenant ID
      "permissions": 15, // The bitmask of permissions for the user's role(s)
      "exp": 1672531800 // Expiration time
    }
    

    This payload is powerful. On every request, our middleware can decode this token and immediately know who the user is, which tenant they belong to, and what they can do (via the permission bitmask from our RBAC design), all without a single database query.

3. Token Refresh

Access tokens are short-lived (e.g., 15 minutes) for security. The refresh token is long-lived (e.g., 30 days) and is used to get a new access token without forcing the user to log in again.

  • Endpoint: POST /api/v1/auth/token/refresh

  • Request Body:

    {
      "refresh_token": "def502002733a17e57..."
    }
    
  • Response (Success - 200 OK): A new token pair.

  • Data Flow & Security (Refresh Token Rotation):

    1. Validate the incoming refresh token.
    2. Check it against a database of issued/revoked refresh tokens.
    3. If valid, immediately mark it as "used" or delete it.
    4. Generate a new access token AND a new refresh token.
    5. Return the new pair to the client.

    This strategy, known as refresh token rotation, is a major security enhancement. If a refresh token is ever leaked, it can only be used once, minimizing the window of attack.

4. User Logout

In a stateless JWT system, true "logout" is primarily a client-side action.

  • Endpoint: POST /api/v1/auth/logout
  • Request Body:
    {
      "refresh_token": "def502002733a17e57..."
    }
    
  • Description: Invalidates the user's session. The client must delete its stored access token. The server will invalidate the long-lived refresh token.
  • Data Flow:
    1. The server receives the refresh token.
    2. It finds the token in the database and revokes it (marks as invalid or deletes).
    3. The client, upon receiving a 200 OK response, is responsible for discarding the access token. Any subsequent requests with that access token will fail after it expires.
This diagram illustrates the session logout flow. While it shows cookies, the principle is the same for our API: the client initiates a logout request, and the server invalidates the session credential (in our case, the refresh token).

Tenant Identification: Connecting the Dots

Once a user is logged in, how does our API know which tenant's data to operate on for every subsequent request? This is where a tenant identification strategy is crucial.

How to Build Multi-Tenant APIs in Node.js

This article provides an excellent overview of tenant identification strategies. Though the code examples are in Node.js, the architectural concepts are universal and directly applicable to FastAPI.

Focus your reading on the section "Tenant Identification Middleware." Analyze the four methods presented: fromSubdomain, fromHeader, fromPathParam, and fromJWT.

We will adopt the fromJWT() strategy as our primary method.

  • Why? It's the most suitable for a pure API backend serving a mobile client. The tenant_id is embedded directly in the access token, making every authenticated request self-contained. Our authentication dependency will extract the user ID and the tenant ID from the token payload, making this information available to the rest of the application for every request.

This brings our design full circle. The login endpoint provides a token containing the tenant_id. All subsequent requests to protected endpoints will use this token. A middleware will extract the tenant_id, and our service/repository layers will use it to scope all database queries, ensuring perfect data isolation.

This diagram shows the flow for a request to a protected endpoint. The client sends a request with the JWT. The API extracts and validates the token, decodes it to get user/tenant info, and then proceeds to check permissions (RBAC) before executing the endpoint logic. This is exactly the flow our design enables.

Conclusion

We have now translated our high-level architectural goals into a concrete low-level design for the services that manage tenants and users. We have defined the API contracts, data flows, and security mechanisms that form the bedrock of our SaaS application.

Key Takeaways:

  • Atomic Registration: New tenants and their first admin user are created together in a single, atomic transaction using the "account-first" principle.
  • Standard-Based Authentication: We've adopted an OAuth 2.0-style token endpoint (/token) that issues a JWT pair (access and refresh tokens).
  • Secure Token Handling: Our design includes short-lived access tokens and long-lived refresh tokens with rotation for enhanced security.
  • Self-Contained Auth: The JWT payload contains the user_id, tenant_id, and permission bitmask, enabling stateless and efficient authorization checks.
  • JWT-based Tenant Identification: We will use the tenant_id claim within the JWT to identify the tenant context for every authenticated API call.

Preview of the Next Lesson:
With our authentication and tenant management foundation designed, we are ready to build the first set of core feature APIs that will be protected by this system. In the next lesson, we will create the low-level design for the project management service, defining API contracts for projects, tasks, and milestones. This is where we'll see our RBAC and multi-tenancy designs in action for the first time.