# How ContiNew Admin Implements Multi-Tenant Data Isolation: Architecture and Code Analysis

> Discover how ContiNew Admin implements multi-tenant data isolation with its architecture. Learn about tenant resolution, context propagation, and code analysis for robust data security. Read now.

- Repository: [OpenContiNew/continew-admin](https://github.com/continew-org/continew-admin)
- Tags: architecture
- Published: 2026-02-27

---

**ContiNew Admin achieves multi-tenant data isolation through a layered architecture combining HTTP header-based tenant resolution, thread-local context propagation, mandatory tenant ID columns in base entities, and scoped execution utilities, all configurable via `TenantExtensionProperties` and overridable using the `@TenantIgnore` annotation.**

ContiNew Admin (continew-org/continew-admin) is an open-source admin template built on Spring Boot that supports multi-tenancy through a shared database schema with row-level security. The system ensures complete data isolation between tenants by intercepting every request, binding a tenant identifier to the executing thread, and automatically injecting that identifier into every database operation.

## Tenant Identification and Configuration

The isolation process begins when a request enters the system. The `DefaultTenantProvider` class reads the tenant identifier from a configurable HTTP header and validates the tenant's operational status before creating a security context.

### Resolving Tenants from HTTP Headers

According to the source code in `continew-plugin/continew-plugin-tenant`, the `DefaultTenantProvider` extracts the tenant code from the header defined in `TenantExtensionProperties` (default: `X-Tenant-Code`). If the header is absent, the system falls back to a default tenant ID (`0`).

```java
@Data
@ConfigurationProperties(prefix = PropertiesConstants.TENANT)
public class TenantExtensionProperties {
    private String tenantCodeHeader = "X-Tenant-Code";
    private Long defaultTenantId = 0L;
    
    public boolean isDefaultTenant() {
        return Objects.equals(TenantContextHolder.getTenantId(), defaultTenantId);
    }
}

```

The provider implementation at [`DefaultTenantProvider.java`](https://github.com/continew-org/continew-admin/blob/main/DefaultTenantProvider.java) (lines 42-73) builds a `TenantContext` object containing the resolved `tenantId`, which is then stored for the duration of the request lifecycle.

### Default Tenant Fallback

When no `X-Tenant-Code` header is present, `TenantExtensionProperties.isDefaultTenant()` checks whether the current context matches the configured `defaultTenantId`. This allows system-wide administrative operations to run without explicit tenant scoping while maintaining the same architectural constraints.

## Thread-Local Context Propagation

Once resolved, the tenant ID must persist throughout the request thread without explicit parameter passing. ContiNew Admin delegates this to `TenantContextHolder` (provided by the **continew-starter-extension-tenant** module), which maintains the tenant state in a thread-local variable.

### Accessing Tenant State in Services

Throughout the codebase, service implementations query the holder to determine whether tenant filtering should apply. For example, in [`MenuServiceImpl.java`](https://github.com/continew-org/continew-admin/blob/main/MenuServiceImpl.java) (lines 71-75), the service checks both whether tenants are enabled globally and whether the current context represents the default tenant before applying menu exclusions.

```java
if (TenantContextHolder.isTenantEnabled() && !tenantExtensionProperties.isDefaultTenant()) {
    query = query == null ? new MenuQuery() : query;
    query.setExcludeMenuIdList(this.listExcludeTenantMenu());
}

```

This pattern ensures that tenant-specific logic executes only when the multi-tenant mode is active and the current user is not operating under the default system tenant.

## Data-Level Isolation Mechanisms

Physical data separation occurs at the entity layer. Every business object that requires tenant isolation extends `TenantBaseDO`, which mandates the presence of a `tenant_id` column in the underlying database tables.

### The TenantBaseDO Base Entity

Located at [`continew-common/src/main/java/top/continew/admin/common/base/model/entity/TenantBaseDO.java`](https://github.com/continew-org/continew-admin/blob/main/continew-common/src/main/java/top/continew/admin/common/base/model/entity/TenantBaseDO.java) (lines 39-42), this base class defines the contractual requirement for tenant-aware entities:

```java
@Data
public class TenantBaseDO extends BaseDO {
    private Long tenantId;
}

```

All repository queries generated by the system's MyBatis-plus integration automatically append `WHERE tenant_id = :tenantId` when `TenantContextHolder.isTenantEnabled()` returns true. This transparent filtering prevents data leakage between tenants without requiring manual SQL modifications in business logic.

### Service-Layer Query Guards

While the ORM handles automatic filtering for direct entity lookups, complex business logic may require explicit tenant checks. The `RoleServiceImpl` class, for instance, inspects `UserContextHolder.isTenantAdmin()` to restrict role visibility, ensuring that tenant administrators cannot view or modify super-administrator roles reserved for the default tenant.

## Scoped Execution for Administrative Tasks

Certain operations—such as initializing a new tenant, cleaning tenant data, or running batch jobs—must execute within a specific tenant context regardless of the HTTP request context. ContiNew Admin provides `TenantUtils.execute()` for this purpose.

### Temporary Context Switching

The `TenantUtils.execute(Long tenantId, Runnable action)` utility temporarily pushes the specified tenant ID onto the `TenantContextHolder`, executes the provided lambda, and then restores the previous context state. This prevents context leakage between sequential operations.

In [`TenantDataApiForSystemImpl.java`](https://github.com/continew-org/continew-admin/blob/main/TenantDataApiForSystemImpl.java) (lines 84-99), the tenant initialization logic uses this utility to create default departments, roles, and users bound to the newly created tenant:

```java
Long tenantId = tenant.getId();
TenantUtils.execute(tenantId, () -> {
    Long deptId = this.initDeptData(tenant);
    Long roleId = this.initRoleData(tenant);
    Long userId = this.initUserData(tenant, roleId, deptId);
    tenantApi.bindAdminUser(tenantId, userId);
});

```

Similarly, tenant cleanup operations wrap all deletion logic inside `TenantUtils.execute()` to ensure that `DELETE` statements filter correctly by `tenant_id` without affecting other tenants' data.

## Bypassing Isolation with @TenantIgnore

Public endpoints that must operate outside tenant scoping—such as authentication, health checks, or captcha generation—use the `@TenantIgnore` annotation to bypass the tenant resolution filter.

### Controller-Level Exemptions

When placed on a controller method, `@TenantIgnore` instructs the tenant resolution filter to skip context creation for that request. The `CommonController.tenantEnabled()` method (lines 86-101) uses this annotation to expose the system's tenant configuration status without requiring a valid tenant header:

```java
@TenantIgnore
@GetMapping("/tenant/enable")
public Boolean tenantEnabled() {
    return TenantContextHolder.isTenantEnabled();
}

```

This mechanism ensures that login flows can authenticate users and resolve their appropriate tenant context dynamically without being blocked by the isolation layer upfront.

## Summary

- **Tenant Resolution**: `DefaultTenantProvider` extracts the tenant ID from the `X-Tenant-Code` header (configurable via `TenantExtensionProperties`) or falls back to a default tenant ID of `0`.
- **Context Propagation**: `TenantContextHolder` maintains the tenant ID in thread-local storage, accessible throughout the service layer via `getTenantId()` and `isTenantEnabled()`.
- **Entity Isolation**: All tenant-scoped entities extend `TenantBaseDO`, which enforces a `tenant_id` column; the ORM automatically filters queries by this column when the tenant context is active.
- **Administrative Utilities**: `TenantUtils.execute()` enables temporary context switching for tenant provisioning and cleanup tasks in `TenantDataApiForSystemImpl`.
- **Selective Bypass**: The `@TenantIgnore` annotation allows specific endpoints like login and health checks to operate without tenant scoping.

## Frequently Asked Questions

### How does ContiNew Admin determine which tenant a request belongs to?

The system examines the HTTP request header specified in `TenantExtensionProperties.tenantCodeHeader` (default `X-Tenant-Code`). The `DefaultTenantProvider` class reads this value, validates the tenant's existence and status, and constructs a `TenantContext` containing the tenant ID. If the header is missing, the provider falls back to the `defaultTenantId` (typically `0`).

### What happens if a database table does not have a tenant_id column?

All entities requiring isolation must extend `TenantBaseDO`, which defines the `private Long tenantId` field mapped to a `tenant_id` database column. If a table lacks this column and the system attempts to perform a tenant-scoped operation, the query will fail. Tables that store global system data (not tenant-specific) should use the base `BaseDO` class instead, though service methods must explicitly handle access control for such entities.

### Can an administrator perform operations across multiple tenants simultaneously?

No, the architecture enforces single-tenant context per thread. However, administrators can use `TenantUtils.execute(tenantId, () -> { ... })` to iterate over tenants sequentially. This utility temporarily sets the tenant context for the duration of the lambda execution, then restores the previous state. The `TenantDataApiForSystemImpl.init()` method demonstrates this pattern when initializing default data for a newly created tenant.

### How do public endpoints like login avoid tenant isolation?

Controller methods annotated with `@TenantIgnore` bypass the tenant resolution filter entirely. This allows endpoints such as `/captcha`, `/auth/login`, and `/tenant/enable` in `CommonController` and `CaptchaController` to execute without a valid tenant header, enabling pre-authentication operations that may need to determine or establish tenant context dynamically.