Organization Isolation
Isolation by Design
Relay is built as a multi-org platform where every resource belongs to exactly one organization. Isolation is enforced at the database level — it's not an afterthought, it's the foundation.
Database-Level Scoping
Every table that contains organization-specific data has an org_id column:
| Table | Columns | Scope |
|---|---|---|
apps | app_id, org_id, name, ... | Apps belong to an organization |
agents | agent_id, org_id, name, ... | Agents belong to an organization |
agent_allowlist | id, org_id, agent_id, app_id, ... | Allowlists are organization-scoped |
events | event_id, org_id, app_id, agent_id, ... | Event logs are organization-scoped |
users | user_id, email, ... | Users are global (but their roles are organization-specific) |
org_members | id, org_id, user_id, role, ... | Roles are per-user per-organization |
Query Scoping Example
Every database query includes WHERE org_id = ?:
-- Get all apps for this organization
SELECT * FROM apps
WHERE org_id = $1
ORDER BY created_at DESC;
-- Check if an app can reach an agent
SELECT CASE
WHEN NOT EXISTS (
SELECT 1 FROM agent_allowlist
WHERE org_id = $1 AND agent_id = $2
) THEN true -- No allowlist = open
WHEN EXISTS (
SELECT 1 FROM agent_allowlist
WHERE org_id = $1 AND agent_id = $2 AND app_id = $3
) THEN true -- App is allowlisted
ELSE false -- App is not allowed
END;
-- Get event logs for this organization
SELECT * FROM events
WHERE org_id = $1
ORDER BY created_at DESC
LIMIT 100;
Key point: It's impossible to query another organization's data without WHERE org_id = ? — the org_id parameter is required.
Application-Level Enforcement
Beyond the database, Relay's API enforces organization isolation:
Token Binding to Organization
When an app or agent authenticates:
App sends: Authorization: Bearer rlk_portal_x8k2m9p...
↓
Relay verifies token
↓
Relay looks up: Which organization owns this token?
↓
Relay: "This app belongs to Organization A"
↓
All subsequent operations are scoped to Organization A
If the same app token were somehow used by Organization B (impossible, but hypothetically):
- ✅ Relay would reject it (wrong token for that organization)
- ✅ Or accept it (same token, but Organization A owns it), and all operations would be in Organization A's scope
WebSocket Session Isolation
When an app connects via WebSocket:
Connection Handshake:
Authorization: Bearer rlk_portal_x8k2m9p...
↓
Relay: Verifies token belongs to Organization A
↓
Session created: { app_id: "portal", org_id: "orgA", ws: ... }
↓
All messages on this connection are processed in Organization A's scope
A WebSocket session is permanently bound to its organization. There's no way to send a message from one organization on another organization's session.
Isolation Guarantees
What Each Organization Can See
Organization A
├── Apps: Portal, Flow (only these)
├── Agents: Athena, Klyve (only these)
├── Allowlists: Only for its own agents
├── Event Logs: Only events from/to its own apps and agents
├── Members: Only users in Organization A
└── ❌ Cannot see Organization B's anything
Organization B
├── Apps: Studio, Academy (only these)
├── Agents: ResearchBot (only this)
├── Allowlists: Only for its own agents
├── Event Logs: Only events from/to its own apps and agents
├── Members: Only users in Organization B
└── ❌ Cannot see Organization A's anything
Practical Isolation Examples
Example 1: Event Logs
Portal (in Organization A) sends an event to Athena. Can Organization B see this event?
Event Details:
event_id: evt_k9p2
org_id: orgA ← Scoped to Organization A
app_id: portal
agent_id: athena
payload: { message: "..." }
Organization B tries: SELECT * FROM events WHERE event_id = 'evt_k9p2'
↓
Query is rewritten: SELECT * FROM events
WHERE event_id = 'evt_k9p2' AND org_id = 'orgB'
↓
Result: No rows (evt_k9p2 belongs to orgA)
↓
Organization B sees nothing
Example 2: Agent Discovery
Portal (in Organization A) calls discovery. Which agents does it see?
Portal sends: { type: "discover" }
↓
Relay gets Portal's organization: orgA
↓
Relay queries: SELECT * FROM agents
WHERE org_id = 'orgA'
↓
Relay returns: [athena, klyve] (only Organization A's agents)
↓
Portal never sees Organization B's ResearchBot
Example 3: Allowlist Restrictions
Klyve in Organization A has an allowlist: [Portal, Flow]. Can Organization B's Studio reach Klyve?
Studio (Organization B) tries: Send event to Klyve (Organization A)
↓
Relay checks: Is Klyve in Organization B? No, it's in Organization A
↓
Relay rejects immediately: AGENT_NOT_FOUND
↓
Studio doesn't even get to the allowlist check
Even if the allowlist had allowed Studio, it doesn't matter — Klyve isn't in Studio's organization.
Cross-Org Isolation (Impossible)
These scenarios are impossible by design:
Can't impersonate another organization
Attacker gets Organization A's app token: rlk_portal_x8k2m9p...
↓
Attacker tries to use it to access Organization B data
↓
Token is bound to Organization A
↓
All operations happen in Organization A's scope
↓
Attacker can't reach Organization B
Can't spoof an org_id
Attacker sends: { org_id: "orgB", ... }
↓
Relay ignores it — org_id comes from the authenticated token, not the message
↓
Organization is determined by the token, not claimed by the client
Can't discover other organizations' agents
App (Organization A) calls discovery: SELECT * FROM agents
↓
Query becomes: SELECT * FROM agents WHERE org_id = 'orgA'
↓
Only Organization A's agents returned
↓
Organization B's agents remain hidden
Can't access other organizations' tokens
All tokens in the database are hashed
↓
Plaintext tokens are shown only at creation, then discarded
↓
Even if a token hash is exposed, it can't be reversed to plaintext
↓
Attacker still can't use it without the plaintext
User Role Isolation
Users can have access to multiple organizations, but their role is per-organization:
User: Alice
├── Organization A: owner (full access)
├── Organization B: member (read-only)
└── Organization C: owner (full access)
When Alice switches to Organization A:
✅ Can register apps/agents
✅ Can manage allowlists
✅ Can view all logs
When Alice switches to Organization B:
✅ Can view dashboard (read-only)
❌ Cannot register apps/agents
❌ Cannot manage anything
When Alice switches to Organization C:
✅ Can do everything (owner in this organization)
Alice's permissions are completely separate in each organization. Being an owner in Organization A doesn't grant any privileges in Organization B.
Testing Isolation
Relay's test suite verifies isolation with tests like:
def test_org_isolation():
# Create two organizations
org_a = create_organization("Acme Corp")
org_b = create_organization("StartupXYZ")
# Create an app in org_a
app_a = create_app(org_a, "Portal")
# Try to access org_a's app as org_b
result = get_app(org_b, app_a.id)
# Should return "not found" (from org_b's perspective)
assert result is None
def test_event_log_isolation():
# Create event in org_a
event = create_event(org_a, app_a, agent_a, payload={...})
# Try to query event as org_b
result = query_events(org_b)
# Should not include event from org_a
assert event not in result
def test_allowlist_isolation():
# Klyve in org_a has allowlist: [portal]
allow_access(org_a, klyve, portal)
# Studio in org_b tries to reach klyve in org_a
can_reach = check_permission(studio, klyve)
# Should be False (klyve is in different organization)
assert can_reach is False
Audit Trail
Every operation that touches resources is logged:
- Who (user email) performed the action
- What (registered app, rotated token, invited member)
- When (timestamp)
- On which organization (org_id)
- Result (success or failure)
This audit trail is also scoped by organization — users only see logs for their own organization.
Incident Response
If a breach occurs:
Scenario: Organization A's app token is exposed
- Only Organization A is affected — token is bound to Organization A
- Rotate the token — new token issued, old is invalidated
- Organization B is unaffected — token can't reach Organization B anyway
- Review Organization A's logs only — other organizations' logs are unaffected
Scenario: Database is breached
- Tokens are hashed — plaintext can't be extracted
- Plaintext is lost — attacker still can't authenticate
- Allowlists are visible — but don't leak sensitive payloads
- Event payloads are exposed — apps should use general context, not secrets
To reduce impact from #4: never send secrets (passwords, API keys, etc.) in event payloads.
Best Practices
1. Don't Share Tokens Across Organizations
Each organization should have its own tokens:
Organization A: rlk_portal_orgA_...
Organization B: rlk_portal_orgB_...
❌ Don't try to use Organization A's token in Organization B
2. Assume Tokens May Be Exposed
Rotate tokens regularly:
- Even if no breach is suspected
- Every 90 days is reasonable
- Immediately if compromised
3. Use Secrets Manager
Don't share tokens as plaintext:
- Use AWS Secrets Manager, Vault, etc.
- Audit who accesses tokens
- Log all access
4. Review Audit Logs Regularly
Check who has done what in your organization:
- Unexpected user invites?
- Tokens rotated by someone you don't know?
- Apps registered without your knowledge?
5. Offboard Users Carefully
When someone leaves:
- Remove them from your organization
- Rotate any tokens they had access to
- Check audit logs for what they did
- Review their access was appropriate
Summary
- All data is scoped by organization at the database level
- Tokens are bound to organizations — impossible to escape
- Queries always include
WHERE org_id = ? - WebSocket sessions are organization-locked
- Cross-org access is impossible by design
- Users can have different roles in different organizations
- Audit logs are organization-scoped
- Rotate tokens regularly to minimize breach impact
Relay's multi-org architecture ensures that organizations are completely isolated from each other. Your data is yours alone.
Next steps: