Skip to main content
Planned architecture: This page describes the intended multi-tenant design for OrgStack. The current codebase provides foundational components (BaseEntity with UUID identifiers) but does not yet implement tenant isolation features.

Overview

OrgStack is designed as a multi-tenant platform where multiple organizations will share the same application infrastructure while maintaining complete data isolation. Each organization (tenant) will operate independently with its own users, roles, and data.

Planned multi-tenant architecture

The planned architecture will use a shared database, shared schema approach with tenant discrimination at the application layer. This will provide cost efficiency while ensuring strict tenant boundaries through code-level isolation.
Every entity in OrgStack includes a tenant identifier to ensure data is properly scoped to the correct organization.

Tenant isolation strategy

OrgStack enforces tenant isolation through multiple layers:
1

Request context

When you authenticate, your JWT token contains the organization ID. Spring Security extracts this and stores it in the request context.
2

Service layer filtering

All service methods automatically filter data by the current tenant. You never query across tenant boundaries.
3

Repository constraints

JPA repositories include tenant ID in all queries. Custom query methods enforce WHERE organizationId = :tenantId clauses.
4

Entity validation

Before persisting any entity, the system validates that it belongs to the authenticated tenant.

Base entity design

All entities in OrgStack extend BaseEntity, which provides foundational fields for multi-tenant operation:
BaseEntity.java
package com.orgstack.common;

import java.time.Instant;
import java.util.UUID;

import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.Getter;

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Getter
public class BaseEntity {
    @Column(nullable = false, updatable = false)
    private UUID id;
    
    @CreatedDate
    @Column(nullable = false, updatable = false)
    private Instant createdAt;
    
    @LastModifiedDate
    @Column(nullable = false)
    private Instant updatedAt;

    protected BaseEntity() {
        this.id = UUID.randomUUID();
    }
}

Key features

  • UUID primary keys: Uses UUID instead of sequential IDs to prevent ID enumeration attacks across tenants
  • Immutable ID: The updatable = false constraint prevents ID changes after creation
  • Audit timestamps: Automatic createdAt and updatedAt tracking via JPA auditing
  • Protected constructor: Forces ID generation at instantiation time
When extending BaseEntity, always add an organizationId field with a NOT NULL constraint and foreign key to the organizations table. Never allow this field to be updated after creation.

Tenant context propagation

The tenant context flows through your application layers:
Controllers extract the organization ID from the authenticated principal (JWT claims) and pass it explicitly to service methods, or rely on a tenant context holder.
Services validate that all operations are scoped to the current tenant. Cross-tenant operations are explicitly forbidden and throw security exceptions.
Repositories add tenant filters to all queries. Spring Data JPA’s @Query annotations include WHERE organizationId = :orgId clauses.

Database configuration

OrgStack uses PostgreSQL with connection pooling optimized for multi-tenant workloads:
application.properties
# Datasource
spring.datasource.url=jdbc:postgresql://localhost:5432/orgstack
spring.datasource.username=orgstack
spring.datasource.password=orgstack_dev_password

# JPA
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.show-sql=false
spring.jpa.properties.hibernate.format_sql=false
spring.jpa.open-in-view=false
The ddl-auto=validate setting ensures schema changes happen through controlled migrations, not automatic Hibernate updates. This is critical for multi-tenant data integrity.

Tenant isolation checklist

When implementing new features, ensure tenant isolation by verifying:
  • Entity includes organizationId column with NOT NULL constraint
  • Entity has foreign key relationship to organizations table
  • Repository queries filter by organizationId
  • Service methods validate tenant ownership before operations
  • Unit tests verify cross-tenant access is blocked
  • Integration tests confirm tenant data isolation

Best practices

1

Never trust client-provided tenant IDs

Always derive the organization ID from the authenticated JWT token, never from request parameters or body.
2

Use explicit tenant scoping

Make tenant filtering explicit in your queries. Avoid relying solely on implicit context that might be bypassed.
3

Test cross-tenant scenarios

Write integration tests that attempt to access data from different tenants. These should always fail with security exceptions.
4

Audit tenant boundaries

Use database constraints and foreign keys to enforce tenant isolation at the database level as a defense-in-depth measure.

Next steps

Authentication

Learn how JWT tokens carry tenant information

Authorization

Understand role-based access control within tenants

Build docs developers (and LLMs) love