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

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:

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:

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:

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:

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:

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

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →