Multi-Tenancy Architecture
Realm-based data isolation and sovereignty enforcement.
Architecture Overview
Autonomy is designed from the ground up as a multi-tenant system where every piece of data belongs to a realm.
This isn't permission checking. This is data isolation enforced by foreign keys, database constraints, and TypeScript types.
Core Principle: Sovereignty by Schema
Traditional multi-tenant systems rely on application-layer permission checks. Autonomy enforces sovereignty at the database schema level.
You can't accidentally leak data across realms because the database won't let you.
How Multi-Tenancy Works
1. Every Entity Has a realm_id
Signals, clusters, and synthesis all have a required realm_id foreign key.
model Signal {
signal_id String @id
realm_id String // REQUIRED - can't be null
// ... other fields
realm Realm @relation(fields: [realm_id], references: [realm_id])
}2. Query Functions Filter by Realm
All query functions accept a userId parameter and automatically filter by realms the user has access to.
// Get user's accessible realms
async function getUserRealmIds(userId: string): Promise<string[]> {
const realms = await prisma.realm.findMany({
where: {
OR: [
{ user_id: userId }, // Realms they own
{ members: { // Realms they're members of
some: { user_id: userId }
}
},
],
},
select: { realm_id: true },
})
return realms.map(r => r.realm_id)
}
// Query signals - automatically filtered
export async function querySignals(
params: QuerySignalsParams,
userId: string
) {
const userRealmIds = await getUserRealmIds(userId)
return await prisma.signal.findMany({
where: {
realm_id: { in: userRealmIds }, // Only accessible realms
// ... other filters
}
})
}3. TypeScript Types Enforce Realm Requirements
Zod validation schemas require realm_id on creation. You can't create an entity without specifying which realm it belongs to.
export const createSignalSchema = z.object({
realm_id: z.string().length(26), // REQUIRED
signal_type: z.enum([...]),
signal_title: z.string(),
// ... other fields
})4. Cross-Realm Operations Are Blocked
You cannot add a signal from one realm to a cluster in another realm. The query layer enforces this.
// Verify signal and cluster share a realm
const signal = await prisma.signal.findUnique({
where: { signal_id },
select: { realm_id: true }
})
const cluster = await prisma.cluster.findUnique({
where: { cluster_id },
select: { realm_id: true }
})
if (signal.realm_id !== cluster.realm_id) {
throw new Error('Cannot add signal to cluster in different realm')
}Realm Access Control
A user has access to a realm if:
- They created it —
realm.user_idmatches theiruser_id - They're a member — Entry exists in
realms_userstable (future feature for shared realms)
Access check implementation:
export async function userHasRealmAccess(
userId: string,
realmId: string
): Promise<boolean> {
const realm = await prisma.realm.findFirst({
where: {
realm_id: realmId,
OR: [
{ user_id: userId },
{ members: { some: { user_id: userId } } }
]
}
})
return realm !== null
}Data Flow with Realm Isolation
Example: Creating a Signal
- 1. User submits signal creation formForm includes
realm_id(pre-selected to default realm) - 2. Validation checks realm_id is presentZod schema requires the field
- 3. Backend verifies user has access to that realmCalls
userHasRealmAccess(userId, realmId) - 4. Signal created with realm foreign keyDatabase enforces the relationship
- 5. Future queries automatically filter by realmUser can only see signals from their accessible realms
Common Query Patterns
Pattern 1: List All Entities
// Get all signals user can access
const userRealmIds = await getUserRealmIds(userId)
const signals = await prisma.signal.findMany({
where: {
realm_id: { in: userRealmIds }
}
})Pattern 2: Get Single Entity
// Get specific signal (verify access)
const userRealmIds = await getUserRealmIds(userId)
const signal = await prisma.signal.findFirst({
where: {
signal_id: signalId,
realm_id: { in: userRealmIds } // Access check
}
})
if (!signal) {
throw new Error('Signal not found or access denied')
}Pattern 3: Update Entity
// Only realm owner can update
const signal = await prisma.signal.findUnique({
where: { signal_id },
include: { realm: true }
})
if (signal.realm.user_id !== userId) {
throw new Error('Only realm owner can update')
}
await prisma.signal.update({
where: { signal_id },
data: updateData
})Pattern 4: Cross-Entity Validation
// Ensure signal and cluster are in same realm
const [signal, cluster] = await Promise.all([
prisma.signal.findUnique({
where: { signal_id },
select: { realm_id: true }
}),
prisma.cluster.findUnique({
where: { cluster_id },
select: { realm_id: true }
})
])
if (signal.realm_id !== cluster.realm_id) {
throw new Error('Signal and cluster must be in same realm')
}Benefits of Schema-Level Isolation
Security by Design
Data leakage is impossible at the schema level. No amount of application bugs can expose cross-realm data.
Type Safety
TypeScript enforces realm_id requirements at compile time. Forget to pass userId? Compiler error.
Clear Ownership
Every entity knows which realm it belongs to. No ambiguity about data ownership.
Scalable Architecture
One user or thousands — the isolation model is identical. Future sharding is straightforward.
Auditability
Every query is scoped to specific realms. Easy to audit who accessed what data.
Future-Proof
Built for shared realms from day one. Adding multi-user collaboration doesn't require schema changes.
Future: Shared Realms
The schema already supports shared realms through the realms_users table.
When implemented, shared realms will enable:
- Multi-user collaboration — Multiple people contributing signals to the same realm
- Role-based permissions — OWNER, CONTRIBUTOR, OBSERVER roles
- Consent-based synthesis — AI analysis across member signals with explicit permission
- The ansible network — Remnant instances communicating across shared realms
The architecture is ready. The feature just needs UI and workflow implementation.
Best Practices
Always pass userId to query functions
Never query without realm filtering. Every query function requires userId.
Verify realm access before mutations
Before update/delete, check user owns the realm or is a member.
Use transactions for cross-entity operations
When creating multiple related entities, wrap in Prisma transaction for atomicity.
Never expose realm_id in URLs
Use entity IDs in routes. realm_id is verified server-side during access checks.