# Multi-Tenant Architecture with Spaces in Bella OpenAPI: How Tenant Isolation Works

> Explore Bella OpenAPI multi-tenant architecture. Discover how Spaces ensure tenant isolation via UUID binding and database query filtering for secure, separated API access.

- Repository: [Ke Technologies/bella-openapi](https://github.com/lianjiatech/bella-openapi)
- Tags: architecture
- Published: 2026-03-06

---

**Bella OpenAPI enforces tenant isolation by treating each Space as a distinct tenant, binding every API request to a Space-specific UUID via thread-local context, and filtering all database queries by the `space_code` column.**

Bella OpenAPI implements a **multi-tenant architecture with Spaces** to provide complete data isolation between organizations. In the `lianjiatech/bella-openapi` repository, each Space acts as an independent tenant with its own resources, members, and API keys. This design ensures that all data access is automatically scoped to the correct tenant through database-level filtering and thread-local context management.

## Understanding the Spaces Abstraction

### What is a Space?

A **Space** represents an independent tenant in Bella OpenAPI. When created via `SpaceService.createSpace()`, each Space receives a unique identifier generated by `SpaceService.generateSpaceCode()` using a random UUID [SpaceService†L12-L14]. This `spaceCode` serves as the canonical tenant identifier throughout the system. The Space is persisted to the `space` table through `SpaceRepo.createSpace()` [SpaceRepo†L44-L50].

### Core Entities Scoped to a Space

Every resource in Bella OpenAPI belongs to exactly one Space. The system maintains isolation through the following scoping mechanisms:

- **API Keys**: The `ApikeyInfo.ownerCode` field stores the Space code, linking each key to its tenant [ApikeyService].
- **Members and Roles**: `SpaceMemberRecord.spaceCode` and `SpaceRoleRecord.spaceCode` associate users with specific Spaces [SpaceRepo].
- **Resources**: Database records such as `VideoJobRecord` include a `space_code` column that ties each row to its owning Space [SpaceRecord].
- **Configurations**: Channels, models, and quotas are fetched using the Space's `ownerCode` and `ownerType` identifiers [CLAUDE.md].

## Tenant Isolation Mechanics

### API Key Authentication and Space Resolution

Tenant isolation begins at the API perimeter. When a request arrives, `ApikeyService.verifyAuth()` hashes the supplied key and returns an `ApikeyInfo` object containing the Space code as `ownerCode` [ApikeyService†L81-L90]. This establishes the tenant context for the entire request lifecycle.

### Thread-Local Context Propagation

The system uses thread-local storage to maintain tenant context across layers. `EndpointContext.setApikey()` stores the `ApikeyInfo` in `BellaContext`, making the Space code accessible to downstream components via `EndpointContext.getApikey()` [EndpointContext†L53-L61]. This pattern ensures that service methods always know which tenant they are operating on behalf of without passing the identifier explicitly through every method signature.

### Database-Level Filtering

Service methods enforce isolation by including the Space code in every database query. For example, `VideoService.createVideoJob()` extracts the Space code from `EndpointContext.getApikey().getOwnerCode()` and persists it in the `video_job.space_code` column [VideoService†L46-L55]. Similarly, `VideoService.listVideoJobs()` filters results to include only rows where `space_code` matches the current tenant [VideoService†L80-L82].

### Authorization and Permission Checks

Beyond data isolation, the system verifies tenant membership before allowing administrative actions. `ApikeyService.checkPermission()` validates that the acting user belongs to the target Space (or holds system administrator privileges) before permitting operations such as role creation or member modification [ApikeyService†L56-L66]. Cross-Space access is strictly prohibited unless using a system-type API key where `ownerType = SYSTEM`, which bypasses standard tenant checks [ApikeyService†L66-L68].

## End-to-End Request Flow

The complete tenant isolation flow works as follows:

1. **Client Authentication**: The client includes an API key in the `Authorization` header.
2. **Interceptor Processing**: `AuthorizationInterceptor` extracts the key, invokes `ApikeyService.verifyAuth()`, and stores the resulting `ApikeyInfo` in `EndpointContext`.
3. **Handler Execution**: The controller (e.g., `VideoController`) retrieves the Space code via `EndpointContext.getApikey().getOwnerCode()`.
4. **Data Access**: All database operations include `space_code` equal to the Space's UUID, guaranteeing physical data segregation.

## Practical Implementation Examples

### Creating a New Space

Use the `SpaceService` to generate a new tenant:

```java
CreateSpaceOp op = new CreateSpaceOp();
op.setSpaceName("Acme Corp");
op.setUserId(123L);                 // the creator's user ID
String spaceCode = spaceService.createSpace(op);   // returns UUID

```

Source: `SpaceService.createSpace()` [SpaceService†L51-L66]

### Generating a Space-Scoped API Key

Bind an API key to a specific Space using the `ownerCode` field:

```java
ApikeyOps.ApplyOp apply = new ApikeyOps.ApplyOp();
apply.setOwnerType("PERSON");          // or "ORG"
apply.setOwnerCode(spaceCode);         // ties the key to the Space
apply.setName("Acme-Video-Key");
String apiKey = apikeyService.apply(apply);

```

Source: `ApikeyService.apply()` [ApikeyService†L18-L27]

### Creating a Resource Within a Space

When authenticated with a Space-specific key, resources automatically inherit the tenant context:

```http
POST /v1/video/create HTTP/1.1
Host: api.bella.openapi.com
Authorization: Bearer <apiKey>
Content-Type: application/json

{
  "model": "video-gen-1",
  "prompt": "A sunrise over the mountains",
  "size": "HD"
}

```

The controller receives the request, extracts the Space code from `EndpointContext.getApikey().getOwnerCode()`, and `VideoService.createVideoJob()` stores it in `video_job.space_code` [VideoService†L46-L55].

### Listing Space Members

Retrieve all users associated with a specific Space:

```java
List<Member> members = spaceService.listMember(spaceCode);
members.forEach(m -> System.out.println(m.getMemberUid() + " → " + m.getRoleCode()));

```

Source: `SpaceService.listMember()` [SpaceService†L14-L20]

### Verifying Tenant Isolation

The following test pattern demonstrates that data is properly segregated:

```java
// Create keys for two different spaces
String keyA = createApiKeyForSpace("spaceA");
String keyB = createApiKeyForSpace("spaceB");

// Create resource in space A
createVideoJob(keyA, request);

// Query with space B key returns empty results
List<VideoJobDB> jobs = listVideoJobs(keyB);
assertTrue(jobs.isEmpty());

```

This works because `listVideoJobs` queries filter by the caller's `space_code` [VideoService†L80-L82].

## Summary

- Bella OpenAPI implements **multi-tenant isolation** through the **Space** abstraction, where each Space receives a unique UUID acting as the tenant identifier.
- **API keys** are bound to Spaces via the `ownerCode` field, and authentication resolves the tenant context through `ApikeyService.verifyAuth()`.
- **Thread-local storage** in `EndpointContext` propagates tenant identity across application layers without explicit parameter passing.
- **Database-level security** is enforced by the `space_code` column present in all tenant-scoped tables, ensuring queries return only data belonging to the current Space.
- **Authorization checks** in `ApikeyService.checkPermission()` prevent cross-tenant administrative actions unless using privileged system keys.

## Frequently Asked Questions

### How is a Space different from a user account?

A **Space** represents the tenant organization itself, while user accounts represent individual members within that tenant. The `SpaceMemberRecord` table links users to Spaces via the `spaceCode` field, allowing a single user to belong to multiple Spaces with different roles (e.g., `OWNER`, `ADMIN`) in each. This distinction enables true multi-tenancy where multiple organizations can share the same infrastructure without data mixing.

### Can an API key access multiple Spaces?

No. Standard API keys are strictly bound to a single Space through the `ownerCode` field set during creation in `ApikeyService.apply()`. While system-level keys with `ownerType = SYSTEM` can bypass tenant checks for administrative operations, regular operational keys are limited to their designated Space as enforced by `ApikeyService.verifyAuth()` and subsequent database filters.

### What happens if the space_code column is missing from a query?

Without the `space_code` filter, the application would violate tenant isolation by returning data across all Spaces. The architecture prevents this by requiring the Space code from `EndpointContext.getApikey()` for all data access operations. Methods like `VideoService.listVideoJobs()` explicitly include `space_code` in their WHERE clauses [VideoService†L80-L82], and the service layer design makes bypassing this filter unlikely.

### How does Bella OpenAPI handle Space creation and Member management?

Space creation occurs through `SpaceService.createSpace()`, which generates a UUID via `generateSpaceCode()` and persists it via `SpaceRepo.createSpace()`. Member management is handled by `SpaceService.listMember()` and related methods that query the `space_member` table using the `spaceCode` as the primary filter. Role-based access control within a Space is enforced through `SpaceRoleRecord` and validated by `ApikeyService.checkPermission()` before any administrative changes are permitted.