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.ownerCodefield stores the Space code, linking each key to its tenant [ApikeyService]. - Members and Roles:
SpaceMemberRecord.spaceCodeandSpaceRoleRecord.spaceCodeassociate users with specific Spaces [SpaceRepo]. - Resources: Database records such as
VideoJobRecordinclude aspace_codecolumn that ties each row to its owning Space [SpaceRecord]. - Configurations: Channels, models, and quotas are fetched using the Space's
ownerCodeandownerTypeidentifiers [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:
- Client Authentication: The client includes an API key in the
Authorizationheader. - Interceptor Processing:
AuthorizationInterceptorextracts the key, invokesApikeyService.verifyAuth(), and stores the resultingApikeyInfoinEndpointContext. - Handler Execution: The controller (e.g.,
VideoController) retrieves the Space code viaEndpointContext.getApikey().getOwnerCode(). - Data Access: All database operations include
space_codeequal 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
ownerCodefield, and authentication resolves the tenant context throughApikeyService.verifyAuth(). - Thread-local storage in
EndpointContextpropagates tenant identity across application layers without explicit parameter passing. - Database-level security is enforced by the
space_codecolumn 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →