Multi-Identity System
Link email, Slack ID, GitHub username to one user profile
MUXI's multi-identity system solves a common problem: users interact through multiple platforms (email, Slack, GitHub), each with its own ID. MUXI automatically links these identities to maintain consistent memory, preferences, and context.
ID format: MUXI-issued user IDs follow usr_{nanoid}; external identifiers retain their native form (email, Slack ID, phone, etc.). Keep the canonical muxi_user_id in your app and attach external identifiers as needed.
The Problem
Users have different IDs across platforms:
Same person, multiple identifiers:
- Email: alice@example.com
- Slack: U123ABC456
- GitHub: alice-dev
- Phone: +1-555-0123
Without multi-identity:
- Each ID = separate user profile
- No shared memory or preferences
- Agent doesn't recognize same person
- Fragmented experience
With multi-identity:
- All IDs → one user profile
- Shared memory across platforms
- Consistent preferences
- Unified experience
How It Works
Identity Resolution
User messages: "Check my GitHub repos"
From: alice@example.com (email)
↓
System checks: Is alice@example.com linked to a user?
↓
Found: Internal user_id = 123, MUXI ID = usr_abc123
↓
Agent uses user 123's memory, credentials, preferences
↓
Response is contextualized for that user
Automatic Linking
First interaction:
alice@example.com → Creates new user (id=123)
Later interaction:
alice@example.com → Found! Uses user 123
Add another identifier:
U123ABC456 (Slack) → Link to user 123
Now both identifiers resolve to same user:
alice@example.com → user 123
U123ABC456 → user 123
Database Schema
Two Tables
users table - Core user entity:
CREATE TABLE users (
id INTEGER PRIMARY KEY, -- Internal ID (fast FK)
public_id VARCHAR(255), -- External MUXI ID (usr_abc123)
formation_id VARCHAR(255), -- Formation isolation
created_at TIMESTAMP,
updated_at TIMESTAMP
);
user_identifiers table - Maps identifiers to users:
CREATE TABLE user_identifiers (
id INTEGER PRIMARY KEY,
user_id INTEGER, -- FK to users.id
identifier VARCHAR(255), -- External ID (email, Slack, etc.)
identifier_type VARCHAR(50), -- Optional type hint
formation_id VARCHAR(255), -- Formation isolation
created_at TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES users(id),
UNIQUE(identifier, formation_id) -- One identifier per formation
);
Key Design
Three ID types:
- Internal ID (
users.id) - Integer, fast database FK - MUXI ID (
users.public_id) - String likeusr_abc123, API-visible - External ID (
user_identifiers.identifier) - Developer-provided (email, Slack, etc.)
Why three IDs?
- Internal ID: Fast database joins
- MUXI ID: Stable external identifier
- External IDs: Multiple per user, platform-specific
Resolution Flow
Fast Path (Cached)
Request: user_id="alice@example.com"
↓
Check cache: "user_id:formation_abc:alice@example.com"
↓
Cache hit: "123:usr_abc123"
↓
Return: (internal_id=123, muxi_id="usr_abc123")
↓
Total time: <1ms
Slow Path (New User)
Request: user_id="alice@example.com"
↓
Check cache: Miss
↓
Query database: Not found
↓
Create new user:
- Generate public_id: usr_abc123
- Insert into users: id=123
↓
Create identifier:
- Insert into user_identifiers
- Link to user_id=123
↓
Cache: "user_id:formation_abc:alice@example.com" = "123:usr_abc123"
↓
Return: (123, "usr_abc123")
↓
Total time: ~50ms (database write)
Slow Path (Existing User)
Request: user_id="U123ABC456" (Slack)
↓
Check cache: Miss
↓
Query:
SELECT u.id, u.public_id
FROM user_identifiers ui
JOIN users u ON ui.user_id = u.id
WHERE ui.identifier = 'U123ABC456'
AND ui.formation_id = 'formation_abc'
↓
Found: (123, "usr_abc123")
↓
Cache result
↓
Return: (123, "usr_abc123")
↓
Total time: ~10ms (database read)
Linking Identifiers
Automatic Linking
When a user provides a new identifier:
# User interacts via email
User: alice@example.com
Agent: "What's your Slack username so I can notify you?"
User: "U123ABC456"
↓
System automatically links:
- alice@example.com → user 123
- U123ABC456 → user 123
Manual Linking
from muxi.runtime.formation import Formation
formation = Formation()
# Link new identifier to existing user
await formation.link_identifier(
user_id="alice@example.com", # Existing identifier
new_identifier="alice-github", # New identifier to link
identifier_type="github" # Optional type hint
)
# Now both resolve to same user
Identifier Types
Optional type hints for clarity:
identifier_types = [
"email",
"slack",
"github",
"phone",
"discord",
"custom"
]
Example:
await formation.link_identifier(
user_id="alice@example.com",
new_identifier="U123ABC456",
identifier_type="slack"
)
Formation Isolation
Same Identifier, Different Formations
Formation A:
alice@example.com → user 123 (Alice Smith)
Formation B:
alice@example.com → user 456 (Different Alice)
Each formation maintains separate user namespaces.
Why?
- Privacy: Formation A can't see Formation B's data
- Flexibility: Same identifier can represent different people
- Security: Complete isolation between formations
Use Cases
Multi-Platform Support
# User interacts via multiple platforms
Email: alice@example.com
Slack: U123ABC456
GitHub: alice-dev
Phone: +1-555-0123
# All linked to one profile
# Consistent memory and preferences across all platforms
Context Preservation
User via email: "I prefer Python over JavaScript"
↓
Stored in user 123's memory
↓
Later, user via Slack: "Generate code for me"
↓
Agent: [Generates Python, remembers preference]
Credential Management
User via email: Adds GitHub credentials
↓
Later, user via Slack: "Show my GitHub repos"
↓
Agent: [Uses same GitHub credentials, recognizes same user]
API Usage
Chat with Identifier
Any identifier resolves to the same user profile:
from muxi import FormationClient
formation = FormationClient(
server_url="http://localhost:7890",
formation_id="my-assistant",
client_key="fmc_..."
)
# Via email identifier
response = formation.chat({"message": "Hello"}, user_id="alice@example.com")
# Via Slack identifier - same user profile
response = formation.chat({"message": "Hello"}, user_id="U123ABC456")
import { FormationClient } from '@muxi-ai/muxi-typescript';
const formation = new FormationClient({
serverUrl: 'http://localhost:7890',
formationId: 'my-assistant',
clientKey: 'fmc_...'
});
// Via email identifier
await formation.chat({ message: 'Hello' }, 'alice@example.com');
// Via Slack identifier - same user profile
await formation.chat({ message: 'Hello' }, 'U123ABC456');
formation := muxi.NewFormationClient(
"http://localhost:7890",
"my-assistant",
"fmc_...",
)
// Via email identifier
formation.Chat("Hello", "alice@example.com")
// Via Slack identifier - same user profile
formation.Chat("Hello", "U123ABC456")
# Via email identifier
curl -X POST http://localhost:8001/v1/chat \
-H "X-Muxi-Client-Key: fmc_..." \
-H "X-Muxi-User-Id: alice@example.com" \
-H "Content-Type: application/json" \
-d '{"message": "Hello"}'
# Via Slack identifier - same user profile
curl -X POST http://localhost:8001/v1/chat \
-H "X-Muxi-Client-Key: fmc_..." \
-H "X-Muxi-User-Id: U123ABC456" \
-H "Content-Type: application/json" \
-d '{"message": "Hello"}'
Link Identifiers
Connect multiple identifiers to one user:
# Link Slack and GitHub to existing email user
formation.link_identifiers(
user_id="alice@example.com",
identifiers=[
("U123ABC456", "slack"),
("alice-dev", "github"),
]
)
# Get all identifiers for a user
identifiers = formation.get_user_identifiers("alice@example.com")
for id in identifiers:
print(f"{id.type}: {id.identifier}")
# email: alice@example.com
# slack: U123ABC456
# github: alice-dev
// Link Slack and GitHub to existing email user
await formation.linkIdentifiers('alice@example.com', [
['U123ABC456', 'slack'],
['alice-dev', 'github'],
]);
// Get all identifiers for a user
const identifiers = await formation.getUserIdentifiers('alice@example.com');
identifiers.forEach(id => console.log(${id.type}: ${id.identifier}));
// Link Slack and GitHub to existing email user
formation.LinkIdentifiers("alice@example.com", []muxi.Identifier{
{ID: "U123ABC456", Type: "slack"},
{ID: "alice-dev", Type: "github"},
})
// Get all identifiers for a user
identifiers, _ := formation.GetUserIdentifiers("alice@example.com")
for _, id := range identifiers {
fmt.Printf("%s: %s
", id.Type, id.ID)
}
# Link identifiers
curl -X POST http://localhost:8001/v1/users/identifiers \
-H "X-Muxi-Client-Key: fmc_..." \
-H "Content-Type: application/json" \
-d '{
"user_id": "alice@example.com",
"identifiers": [
["U123ABC456", "slack"],
["alice-dev", "github"]
]
}'
# Get user identifiers
curl http://localhost:8001/v1/users/alice@example.com/identifiers \
-H "X-Muxi-Client-Key: fmc_..."
Remove Identifier
# Unlink an identifier
formation.remove_identifier("alice-old-email@example.com")
# User still accessible via other identifiers
// Unlink an identifier
await formation.removeIdentifier('alice-old-email@example.com');
curl -X DELETE http://localhost:8001/v1/users/identifiers/alice-old-email@example.com \
-H "X-Muxi-Client-Key: fmc_..."
Building an identity dashboard
1) Store the canonical muxi_user_id returned on first interaction (or create one via /users/identifiers).
2) Let users attach more IDs (Slack, email, GitHub, phone) by calling POST /users/identifiers with strings, [id, type], or objects.
3) Show linked IDs via GET /users/identifiers; allow removal with DELETE /users/identifiers/{identifier}.
4) Always send whatever identifier the request came from in X-Muxi-User-Id so all channels resolve to the same profile.
Performance
Caching
Cache key: user_id:formation_id:identifier
Cache TTL: 1 hour (configurable)
Hit rate: >95% after warmup
Database Indexes
-- Fast identifier lookup
CREATE INDEX idx_user_identifiers_lookup
ON user_identifiers(identifier, formation_id);
-- Fast user reverse lookup
CREATE INDEX idx_user_identifiers_user
ON user_identifiers(user_id);
Benchmarks
| Operation | Cold (no cache) | Warm (cached) |
|---|---|---|
| Resolve existing user | ~10ms | <1ms |
| Create new user | ~50ms | N/A |
| Link identifier | ~30ms | N/A |
Configuration
Enable Multi-Identity
# formation.afs
multi_identity:
enabled: true
cache_ttl: 3600 # 1 hour
Database Configuration
PostgreSQL (recommended for production):
memory:
persistent:
connection_string: ${{ secrets.POSTGRES_URI }}
SQLite (development):
memory:
persistent:
connection_string: "sqlite:///./formation.db"
Migration
From Single User to Multi-Identity
# Existing single-user formation
# user_id was always "0"
# Enable multi-identity
# Old "0" identifier automatically migrated
# Add new identifiers
await formation.link_identifier(
user_id="0", # Old identifier
new_identifier="alice@example.com",
identifier_type="email"
)
# Now both work:
# - "0" (backward compatible)
# - "alice@example.com" (new)
Bulk Import
# Import existing user mappings
mappings = [
("alice@example.com", "U123ABC", "slack"),
("bob@example.com", "U456DEF", "slack"),
# ...
]
for email, slack_id, id_type in mappings:
await formation.link_identifier(
user_id=email,
new_identifier=slack_id,
identifier_type=id_type
)
Debugging
List All Users
# Get all users in formation
users = await formation.list_users()
for user in users:
print(f"User: {user.public_id}")
print(f"Identifiers: {user.identifiers}")
print(f"Created: {user.created_at}")
Trace Resolution
import logging
logging.getLogger('muxi.identity').setLevel(logging.DEBUG)
# Shows resolution flow:
# "Resolving alice@example.com"
# "Cache miss, querying database"
# "Found user 123 (usr_abc123)"
# "Cached for 1 hour"
Best Practices
1. Use Stable Identifiers
✅ Good:
- Email addresses
- OAuth IDs (GitHub, Google)
- Phone numbers
❌ Bad:
- Session IDs (temporary)
- Request IDs (per-request)
- Temporary tokens
2. Link Early
# Ask for additional identifiers early
User: "Hello" (via email)
Agent: "To help you better across platforms, what's your Slack username?"
User: "U123ABC"
Agent: [Links identifiers automatically]
3. Type Hints
# Always provide type hints
await formation.link_identifier(
user_id="alice@example.com",
new_identifier="U123ABC",
identifier_type="slack" # ✓ Clear what this is
)
4. Monitor Linkages
# Periodically audit user identifiers
users_with_multiple = await formation.get_users_with_multiple_identifiers()
for user in users_with_multiple:
if len(user.identifiers) > 5:
logger.warning(f"User {user.public_id} has {len(user.identifiers)} identifiers")
Security
Verification
Before linking identifiers, verify ownership:
# Send verification code
code = await formation.send_verification_code(
identifier="alice-new@example.com",
method="email"
)
# User provides code
if await formation.verify_code(code, user_input):
# Link identifier
await formation.link_identifier(...)
Audit Trail
-- Track identifier changes
CREATE TABLE user_identifier_audit (
id INTEGER PRIMARY KEY,
user_id INTEGER,
action VARCHAR(50), -- 'linked', 'unlinked'
identifier VARCHAR(255),
timestamp TIMESTAMP,
ip_address VARCHAR(45)
);
Limitations
No Automatic Merging
If two identifiers accidentally create separate users:
alice@example.com → user 123
alice-slack → user 456
# These are separate users, not automatically merged
Solution: Manually link or merge:
await formation.merge_users(
primary_user="alice@example.com",
merge_user="alice-slack"
)
Formation Scoped
Identifiers don't transfer between formations:
Formation A: alice@example.com → user 123
Formation B: alice@example.com → user 456 (different)
Each formation is isolated by design.
Learn More
- User Credentials - Per-user credential storage
- Memory System - User-specific memory
- Multi-Tenancy - User isolation architecture