Overview
Multi-user applications need complete data isolation between users while sharing common infrastructure. Trix's space-based architecture makes this straightforward: each user gets their own space, and memories never leak across boundaries.
This guide covers patterns for user isolation, shared resources, access control, and lifecycle management in production multi-tenant applications.
One Space Per User
The fundamental pattern for multi-user apps: create a dedicated space for each user. This provides complete isolation at the data layer.
import { Trix } from '@trixdb/client';
const client = new Trix({ apiKey: process.env.TRIX_API_KEY });
class UserMemoryService {
async onUserCreated(userId: string, userName: string) {
const space = await client.spaces.create({
name: `user-${userId}`,
description: `Memory space for ${userName}`
});
await db.users.update(userId, { trixSpaceId: space.id });
return space;
}
async getSpaceId(userId: string): Promise<string> {
const user = await db.users.get(userId);
return user.trixSpaceId;
}
async remember(userId: string, content: string, metadata?: object) {
const spaceId = await this.getSpaceId(userId);
return client.memories.create({ content, spaceId, metadata });
}
async recall(userId: string, query: string, limit = 10) {
const spaceId = await this.getSpaceId(userId);
return client.memories.list({ q: query, spaceId, limit });
}
}With this pattern, a query for User A's memories will never return User B's data, even if the query text matches.
Hierarchical Isolation
For complex organizations, implement hierarchical isolation: organization → team → user.
interface SpaceHierarchy {
orgSpaceId: string;
teamSpaceId: string;
userSpaceId: string;
}
class HierarchicalMemoryService {
async getHierarchy(userId: string): Promise<SpaceHierarchy> {
const user = await db.users.get(userId);
const team = await db.teams.get(user.teamId);
const org = await db.orgs.get(team.orgId);
return {
orgSpaceId: org.trixSpaceId,
teamSpaceId: team.trixSpaceId,
userSpaceId: user.trixSpaceId
};
}
async recall(userId: string, query: string) {
const hierarchy = await this.getHierarchy(userId);
const [userResults, teamResults, orgResults] = await Promise.all([
client.memories.list({ q: query, spaceId: hierarchy.userSpaceId, limit: 5 }),
client.memories.list({ q: query, spaceId: hierarchy.teamSpaceId, limit: 3 }),
client.memories.list({ q: query, spaceId: hierarchy.orgSpaceId, limit: 2 })
]);
return [
...userResults.data.map(m => ({ ...m, source: 'user' })),
...teamResults.data.map(m => ({ ...m, source: 'team' })),
...orgResults.data.map(m => ({ ...m, source: 'org' }))
];
}
}Access Control Patterns
Use Trix API keys with space-scoped permissions for fine-grained access control.
async function createUserApiKey(userId: string) {
const userSpaceId = await getSpaceId(userId);
const apiKey = await adminClient.apiKeys.create({
name: `user-${userId}-key`,
permissions: {
spaces: [{ spaceId: userSpaceId, access: 'read_write' }]
}
});
return apiKey;
}
async function createSessionToken(userId: string) {
const userSpaceId = await getSpaceId(userId);
return adminClient.tokens.create({
spaceId: userSpaceId,
access: 'read',
expiresIn: '1h'
});
}Never expose your admin API key to clients. Create scoped keys or short-lived tokens for client-side access.
User Onboarding
Initialize new user spaces with helpful starting content.
async function onboardUser(userId: string, profile: UserProfile) {
const space = await client.spaces.create({
name: `user-${userId}`,
description: `Memories for ${profile.name}`
});
const initialMemories = [
{ content: `User's name is ${profile.name}`, tags: ['profile'] },
{ content: `User's email is ${profile.email}`, tags: ['profile'] },
{ content: `User signed up on ${new Date().toISOString()}`, tags: ['profile'] }
];
await Promise.all(
initialMemories.map(m =>
client.memories.create({ ...m, spaceId: space.id })
)
);
return space;
}User Offboarding
Handle account deletion and data export for GDPR compliance.
class UserDataService {
async exportUserData(userId: string) {
const spaceId = await this.getSpaceId(userId);
const allMemories = [];
let cursor: string | undefined;
do {
const result = await client.memories.list({ spaceId, limit: 100, cursor });
allMemories.push(...result.data);
cursor = result.next_cursor;
} while (cursor);
return {
userId,
exportedAt: new Date().toISOString(),
memoryCount: allMemories.length,
memories: allMemories
};
}
async deleteUserData(userId: string) {
const spaceId = await this.getSpaceId(userId);
await client.spaces.delete(spaceId);
await db.users.update(userId, { trixSpaceId: null });
return { deleted: true, spaceId };
}
}Deleting a space automatically deletes all memories within it. This makes GDPR compliance straightforward.
Session-Based Isolation
For applications where each conversation should be isolated, use clusters within a user's space.
class ConversationMemoryService {
async startConversation(userId: string) {
const spaceId = await this.getSpaceId(userId);
const conversationId = crypto.randomUUID();
const cluster = await client.clusters.create({
name: `conversation-${conversationId}`,
spaceId,
metadata: { startedAt: new Date().toISOString() }
});
return { conversationId, clusterId: cluster.id };
}
async rememberInConversation(userId: string, clusterId: string, content: string) {
const spaceId = await this.getSpaceId(userId);
return client.memories.create({
content, spaceId, clusterId,
metadata: { source: 'conversation' }
});
}
}Cross-User Analytics
Aggregate insights across users without exposing individual data.
class AnalyticsService {
async getUsageStats() {
const spaces = await client.spaces.list();
const stats = await Promise.all(
spaces.data.map(async (space) => {
const details = await client.spaces.get(space.id);
return {
spaceId: space.id,
memoryCount: details.memory_count,
createdAt: space.created_at
};
})
);
return {
totalSpaces: spaces.data.length,
totalMemories: stats.reduce((sum, s) => sum + s.memoryCount, 0),
averageMemoriesPerUser: stats.reduce((sum, s) => sum + s.memoryCount, 0) / stats.length
};
}
}Space-level statistics provide useful product metrics without accessing individual memory content.
Performance at Scale
Strategies for handling thousands of users efficiently.
- Cache space IDs - Map user IDs to space IDs in Redis to avoid database lookups
- Batch operations - When creating multiple memories, use parallel requests
- Connection pooling - Reuse the Trix client instance across requests
- Async processing - Store memories asynchronously after responding to users
class CachedUserService {
private redis: Redis;
private CACHE_TTL = 3600;
async getSpaceId(userId: string): Promise<string> {
const cacheKey = `trix:space:${userId}`;
let spaceId = await this.redis.get(cacheKey);
if (spaceId) return spaceId;
const user = await db.users.get(userId);
spaceId = user.trixSpaceId;
await this.redis.setex(cacheKey, this.CACHE_TTL, spaceId);
return spaceId;
}
}Best Practices
- Always scope by space - Never query without a spaceId in multi-user apps
- Use meaningful space names - Include user identifiers for easier debugging
- Handle missing spaces gracefully - Check if space exists before querying
- Implement rate limiting per user - Prevent abuse in shared infrastructure
- Log space IDs, not content - Audit trails without privacy concerns
- Plan for data portability - Design export/delete from day one