User Profiles
Last Updated: 2025-10-28
Rich user context and preferences that persist across all agents and conversations.
Overview
User profiles exist for ONE critical reason: GDPR-compliant cascade deletion.
Cortex Cloud Feature: Automatic cascade deletion is available in Cortex Cloud. Direct Mode can achieve GDPR compliance through manual deletion from each store.
Cortex Cloud: One API call removes user data from every store across all layers that contains an explicit userId reference - cortex.users.delete(userId, { cascade: true }).
Direct Mode: Achieves the same result through manual deletion loops (see Pattern 4 below for implementation).
Secondary benefit: Provides a semantic, user-friendly API for managing user data (cortex.users.get() instead of cortex.immutable.get('user', ...)).
Under the hood: User profiles are stored in cortex.immutable.* with type='user'. The cortex.users.* API is a specialized wrapper that adds cross-layer GDPR deletion capabilities (Cloud Mode) or convenience methods (Direct Mode).
Core Concept: GDPR Cascade Deletion
Cortex Cloud Only: Automatic cascade deletion requires Cortex Cloud connection.
Cortex Cloud enables one-click deletion of all user data across the entire system:
// Cortex Cloud: GDPR "right to be forgotten" - ONE call
const result = await cortex.users.delete("user-123", { cascade: true });
// Automatically deletes from:
// ✅ User profile (immutable type='user')
// ✅ Layer 1a: All conversations with userId='user-123'
// ✅ Layer 1b: All immutable records with userId='user-123'
// ✅ Layer 1c: All mutable keys with userId='user-123'
// ✅ Layer 2: All vector memories with userId='user-123' (across ALL agents)
console.log(`Total records deleted: ${result.totalRecordsDeleted}`);
// Could be hundreds or thousands of records across all stores
Why Cortex Cloud cascade matters:
- ✅ 1 line of code vs ~40 lines manual (saves development time)
- ✅ Atomic deletion (all or nothing transaction)
- ✅ Complete audit trail automatically generated
- ✅ Granular control (preserve specific layers if needed)
- ✅ No missed stores (automatic discovery)
- ✅ Enterprise-ready compliance documentation
Architecture:
Layer 1: ACID Stores (all support optional userId)
├── conversations.* (userId: optional) ← GDPR cascade target
├── immutable.* (userId: optional) ← USER PROFILES stored here + cascade target
└── mutable.* (userId: optional) ← GDPR cascade target
Layer 2: Vector Index (supports optional userId)
└── vector.* (userId: optional) ← GDPR cascade target
Convenience API:
└── users.* (immutable wrapper + GDPR cascade engine)
Secondary benefit - Semantic API:
// Convenience
await cortex.users.get("user-123");
// vs Equivalent
await cortex.immutable.get("user", "user-123");
User Profile Structure
User profiles have a flexible structure - only id is required:
interface UserProfile {
// Identity (REQUIRED)
id: string; // User ID
// User Data (FLEXIBLE - any structure you want!)
data: Record<string, any>; // Completely customizable
// System fields (automatic)
version: number;
createdAt: Date;
updatedAt: Date;
previousVersions?: UserVersion[];
}
interface UserVersion {
version: number;
data: Record<string, any>;
timestamp: Date;
}
Suggested Convention (but not enforced):
// Common pattern for user data structure
await cortex.users.update("user-123", {
data: {
displayName: "Alex Johnson", // Display name
email: "alex@example.com", // Contact
// Preferences (your structure)
preferences: {
theme: "dark",
language: "en",
timezone: "America/New_York",
communicationStyle: "friendly",
},
// Metadata (your structure)
metadata: {
tier: "pro",
signupDate: new Date(),
company: "Acme Corp",
},
// Add ANY custom fields
customField: "anything you want!",
},
});
Automatic Versioning: Like
immutable.*stores, user profiles automatically preserve previous versions when updated. Track how user preferences change over time.
Under the Hood:
cortex.users.update()callscortex.immutable.store()withtype='user'.
Basic Operations
Creating a Profile
// Create or update user profile
await cortex.users.update("user-123", {
data: {
displayName: "Alex Johnson",
email: "alex@example.com",
preferences: {
theme: "dark",
language: "en",
timezone: "America/Los_Angeles",
communicationStyle: "friendly",
},
tier: "pro",
signupDate: new Date(),
company: "Acme Corp",
},
});
Retrieving a Profile
// Get user profile
const user = await cortex.users.get("user-123");
console.log(user.data.displayName); // "Alex Johnson"
console.log(user.data.preferences.theme); // "dark"
console.log(user.data.tier); // "pro"
console.log(user.version); // Version number
Updating Profiles
Updates automatically preserve previous versions:
// Original profile
await cortex.users.update("user-123", {
data: {
displayName: "Alex",
preferences: {
theme: "dark",
language: "en",
},
},
});
// Update theme (creates v2, preserves v1)
await cortex.users.update("user-123", {
data: {
preferences: {
theme: "light", // Only updates theme, merges with existing
},
},
});
// Get current with history
const user = await cortex.users.get("user-123");
console.log(user.version); // 2
console.log(user.data.preferences.theme); // 'light'
console.log(user.previousVersions[0].data.preferences.theme); // 'dark'
// Update last seen (skip versioning for routine stats)
await cortex.users.update(
"user-123",
{
data: {
lastSeen: new Date(),
},
},
{
skipVersioning: true, // Don't create version for stats
},
);
Deleting Profiles
// Delete user profile only
await cortex.users.delete("user-123");
// Delete with cascade (also delete user's data in all agent memories)
const result = await cortex.users.delete("user-123", {
cascade: true, // Delete from all agents
});
console.log(result);
// {
// profileDeleted: true,
// memoriesDeleted: 145,
// agentsAffected: ['agent-1', 'agent-2', 'agent-3'],
// deletedAt: Date
// }
// GDPR-compliant deletion with audit trail
async function handleGDPRDeletion(userId: string, requestedBy: string) {
// Log the request
await auditLog.record({
action: "gdpr-deletion-request",
userId,
requestedBy,
timestamp: new Date(),
});
// Delete everything
const result = await cortex.users.delete(userId, {
cascade: true,
auditReason: "GDPR right to be forgotten request",
});
// Log completion
await auditLog.record({
action: "gdpr-deletion-complete",
userId,
...result,
});
return result;
}
Using Profiles with Agents
Access User Preferences
async function respondToUser(
memorySpaceId: string,
userId: string,
message: string,
) {
// Get user profile
const user = await cortex.users.get(userId);
if (!user) {
// Create default profile on first interaction
await cortex.users.update(userId, {
displayName: userId, // Temporary
preferences: {
language: detectLanguage(message),
timezone: "UTC",
},
metadata: {
tier: "free",
signupDate: new Date(),
firstMessage: message,
},
});
user = await cortex.users.get(userId);
}
// Adapt response based on preferences
let response = await generateResponse(message);
// Apply communication style
if (user.preferences.communicationStyle === "formal") {
response = makeFormal(response);
}
// Localize if needed
if (user.preferences.language !== "en") {
response = await translate(response, user.preferences.language);
}
// Update last seen
await cortex.users.update(
userId,
{
metadata: { lastSeen: new Date() },
},
{ skipVersioning: true },
);
return response;
}
Personalization
async function personalizeExperience(userId: string) {
const user = await cortex.users.get(userId);
return {
greeting: `Hello ${user.displayName}!`,
theme: user.preferences.theme || "light",
timezone: user.preferences.timezone || "UTC",
isPro: user.metadata.tier === "pro",
};
}
Cross-Agent Context
All agents can access user profile:
// Support agent uses profile
const user = await cortex.users.get(userId);
const greeting = `Good ${getTimeOfDay(user.preferences.timezone)}, ${user.displayName}!`;
// Sales agent uses same profile
const user = await cortex.users.get(userId);
if (user.metadata.tier === "free") {
offerUpgrade();
}
// Billing agent uses same profile
const user = await cortex.users.get(userId);
sendInvoiceTo(user.email);
Advanced Features
Profile Schemas
Define custom profile schemas:
// Define your user structure
interface CustomUserProfile extends UserProfile {
preferences: {
theme: "light" | "dark";
language: "en" | "es" | "fr";
emailFrequency: "daily" | "weekly" | "never";
featuresEnabled: string[];
};
metadata: {
tier: "free" | "pro" | "enterprise";
credits: number;
lastPurchase?: Date;
referralCode?: string;
};
}
// Use with type safety
const user = await cortex.users.get<CustomUserProfile>(userId);
console.log(user.preferences.emailFrequency); // Typed!
Nested Preferences
Organize complex preferences:
await cortex.users.update(userId, {
preferences: {
notifications: {
email: true,
push: false,
sms: false,
frequency: "weekly",
},
privacy: {
shareData: false,
analytics: true,
marketing: false,
},
ui: {
theme: "dark",
density: "comfortable",
animations: true,
},
},
});
// Access nested values
const user = await cortex.users.get(userId);
if (user.preferences.notifications.email) {
sendEmail(user.email, notification);
}
Profile Validation
Validate before updating:
import { z } from "zod";
const UserProfileSchema = z.object({
displayName: z.string().min(1).max(100),
email: z.string().email().optional(),
preferences: z
.object({
theme: z.enum(["light", "dark"]).optional(),
language: z.string().length(2).optional(),
timezone: z.string().optional(),
})
.optional(),
metadata: z.record(z.any()).optional(),
});
async function safeUpdateProfile(userId: string, data: any) {
// Validate
const validated = UserProfileSchema.parse(data);
// Update
return await cortex.users.update(userId, validated);
}
Real-World Patterns
Pattern 1: Initialize on First Contact
async function handleFirstMessage(userId: string, message: string) {
// Check if profile exists
let user = await cortex.users.get(userId);
if (!user) {
// Create default profile
await cortex.users.update(userId, {
displayName: userId, // Temporary until we know their name
preferences: {
language: detectLanguage(message),
timezone: "UTC",
},
metadata: {
tier: "free",
signupDate: new Date(),
firstMessage: message,
},
});
user = await cortex.users.get(userId);
}
return user;
}
Pattern 2: Progressive Enhancement
Build user profiles over time:
async function learnFromConversation(userId: string, conversation: string) {
const user = await cortex.users.get(userId);
// Extract information from conversation
const insights = await extractUserInsights(conversation);
// Update profile incrementally
await cortex.users.update(userId, {
displayName: insights.name || user.displayName,
preferences: {
...user.preferences,
...insights.preferences,
},
metadata: {
...user.metadata,
lastSeen: new Date(),
conversationCount: (user.metadata.conversationCount || 0) + 1,
},
});
}
Pattern 3: User Preferences UI
Sync with user-facing preferences:
// User updates preferences in your UI
async function handlePreferenceChange(userId: string, changes: any) {
await cortex.users.update(userId, {
preferences: changes,
});
// All agents immediately see the change
const user = await cortex.users.get(userId);
applyPreferences(user.preferences);
}
Pattern 4: GDPR Compliance
Cortex Cloud (Automatic):
async function handleDataDeletionRequest(userId: string) {
// Cortex Cloud: One-click cascade deletion
const result = await cortex.users.delete(userId, {
cascade: true,
auditReason: "GDPR right to be forgotten request",
});
console.log(`GDPR deletion complete for user ${userId}`);
console.log(`- Profile deleted: ${result.profileDeleted}`);
console.log(`- Total records: ${result.totalRecordsDeleted}`);
console.log(`- Agents affected: ${result.agentsAffected.join(", ")}`);
return result;
// Done in 1 line! ✅
}
Direct Mode (Manual):
async function manualGDPRDeletion(userId: string) {
const deletionLog = [];
// 1. Export user data first (GDPR requirement)
const agents = await cortex.agents.list();
for (const agent of agents) {
const userData = await cortex.memory.export(agent.id, {
userId: userId,
format: "json",
});
if (userData.length > 0) {
await saveToFile(`gdpr-export-${userId}-${agent.id}.json`, userData);
}
}
// 2. Delete from all agents using deleteMany
for (const agent of agents) {
const result = await cortex.memory.deleteMany(agent.id, {
userId: userId, // Universal filter!
});
if (result.deleted > 0) {
deletionLog.push({
memorySpaceId: agent.id,
deleted: result.deleted,
});
}
}
// 3. Delete user profile
await cortex.users.delete(userId);
console.log(`Deleted all data for user ${userId}`);
return deletionLog;
}
Querying User Profiles
Search Users
Find users with filters (same pattern as memory operations):
// Find all pro users
const proUsers = await cortex.users.search({
metadata: { tier: "pro" },
});
// Find users by company
const companyUsers = await cortex.users.search({
metadata: { company: "Acme Corp" },
});
// Find inactive users
const inactive = await cortex.users.search({
metadata: {
lastSeen: {
$lte: new Date(Date.now() - 90 * 24 * 60 * 60 * 1000),
},
},
});
// Find users with specific preferences
const darkModeUsers = await cortex.users.search({
preferences: { theme: "dark" },
});
List Users (Paginated)
// List all users
const page1 = await cortex.users.list({
limit: 50,
offset: 0,
sortBy: "createdAt",
sortOrder: "desc",
});
// List with filters
const recentUsers = await cortex.users.list({
metadata: {
signupDate: {
$gte: new Date("2025-10-01"),
},
},
limit: 100,
});
Count Users
// Total user count
const total = await cortex.users.count();
// Count by tier
const proCount = await cortex.users.count({
metadata: { tier: "pro" },
});
// Count active users (last 30 days)
const activeCount = await cortex.users.count({
metadata: {
lastSeen: {
$gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
},
},
});
Bulk Operations
// Update multiple users
await cortex.users.updateMany(
{
metadata: { tier: "free" },
},
{
preferences: {
newFeatureEnabled: true,
},
},
);
// Delete inactive free users
await cortex.users.deleteMany({
metadata: {
tier: "free",
lastSeen: {
$lte: new Date(Date.now() - 365 * 24 * 60 * 60 * 1000),
},
},
});
Multi-Tenant Considerations
Tenant Isolation
If building multi-tenant apps, include tenant ID:
// User ID includes tenant
const userId = `${tenantId}:${userLocalId}`;
// Or use metadata
await cortex.users.update(userId, {
displayName: "Alex",
metadata: {
tenantId: "tenant-abc",
role: "admin",
},
});
// Query by tenant (universal filters work here too!)
const tenantUsers = await cortex.users.search({
metadata: { tenantId: "tenant-abc" },
});
// Count users per tenant
const count = await cortex.users.count({
metadata: { tenantId: "tenant-abc" },
});
// Export tenant data (includes user profiles, can link to memories)
const tenantData = await cortex.users.export({
metadata: { tenantId: "tenant-abc" },
format: "json",
includeMemories: true, // Optional: export user's memories too
});
// Export will include:
// - User profiles
// - Associated vector memories (if includeMemories=true)
// - conversationRef links to ACID conversations
// - Can optionally export full conversations from ACID
Note: User profiles are NOT stored in ACID conversations or vector memories. They're a separate entity type in Convex, but memories can reference users via
userIdfield.
Profile Version History
Viewing Profile Changes
// Get current profile
const user = await cortex.users.get("user-123");
console.log(`Current version: ${user.version}`);
console.log(`Display name: ${user.displayName}`);
console.log(`Theme: ${user.preferences.theme}`);
// View all versions
user.previousVersions?.forEach((v) => {
console.log(`v${v.version} (${v.timestamp}):`);
console.log(` Display name: ${v.displayName}`);
console.log(` Theme: ${v.preferences?.theme}`);
});
Get Specific Version
// What were user's preferences on a specific date?
const historicalProfile = await cortex.users.getAtTimestamp(
"user-123",
new Date("2025-08-01"),
);
console.log("Theme in August:", historicalProfile.preferences.theme);
Track Preference Changes
// Analyze how preferences evolved
async function analyzePreferenceChanges(userId: string) {
const user = await cortex.users.get(userId);
const changes = [];
let previous = user.previousVersions?.[user.previousVersions.length - 1];
for (const version of [...(user.previousVersions || []), user]) {
if (previous) {
// Detect changes
if (version.preferences?.theme !== previous.preferences?.theme) {
changes.push({
field: "theme",
from: previous.preferences?.theme,
to: version.preferences?.theme,
when: version.timestamp || version.updatedAt,
});
}
}
previous = version;
}
return changes;
}
Profile Analytics
Usage Tracking
// Track profile access
const user = await cortex.users.get(userId);
// Automatically tracked:
console.log({
createdAt: user.createdAt,
updatedAt: user.updatedAt,
version: user.version,
lastSeen: user.metadata.lastSeen,
});
// Log access for analytics
await analytics.track("profile-accessed", {
userId,
timestamp: new Date(),
accessedBy: agentId,
});
Profile Completeness
Measure how complete a profile is:
function calculateCompleteness(user: UserProfile): number {
const fields = [
user.displayName,
user.email,
user.preferences.theme,
user.preferences.language,
user.preferences.timezone,
];
const filled = fields.filter((f) => f !== undefined && f !== null).length;
return (filled / fields.length) * 100;
}
const user = await cortex.users.get(userId);
const completeness = calculateCompleteness(user);
if (completeness < 50) {
console.log("Profile incomplete - prompt user for more info");
}
Cloud Mode Features
Cloud Mode Only: Enhanced profile features with Cortex Cloud
Profile Analytics Dashboard
- User engagement metrics
- Preference trends
- Profile completeness scores
- User segmentation
Smart Defaults
AI-powered default suggestions:
- "Most users in this region prefer timezone X"
- "Users with similar usage patterns prefer Y"
- Auto-detect language from messages
Profile Synchronization
Sync with external systems:
- Auth0, Clerk, Supabase Auth
- CRM systems (Salesforce, HubSpot)
- Identity providers (Okta, Azure AD)
Universal Filters for Users
Core Principle: User operations support the same filter patterns as memory operations
// The same filters work for:
const filters = {
metadata: {
tier: "pro",
signupDate: { $gte: new Date("2025-01-01") },
},
preferences: {
language: "en",
},
};
// Search
await cortex.users.search(filters);
// Count
await cortex.users.count(filters);
// List
await cortex.users.list(filters);
// Update many
await cortex.users.updateMany(filters, { metadata: { reviewed: true } });
// Delete many
await cortex.users.deleteMany(filters);
// Export
await cortex.users.export(filters);
Supported Filters:
metadata.*- Any metadata field with operators ($gte, $lte, $eq, etc.)preferences.*- Any preference fieldcreatedBefore/After- Date range for creationupdatedBefore/After- Date range for updatesemail- Email address (exact or pattern match)displayName- Name (exact or pattern match)
Best Practices
1. Minimal Required Fields
Only require what you truly need:
// ✅ Start minimal
await cortex.users.update(userId, {
displayName: "Alex", // That's it!
});
// Add more over time as you learn
2. Default Values
Provide sensible defaults:
const defaultPreferences = {
theme: "light",
language: "en",
timezone: "UTC",
notifications: true,
};
await cortex.users.update(userId, {
displayName: name,
preferences: { ...defaultPreferences, ...customPreferences },
});
3. Update Timestamp
Track when user was last seen:
async function recordUserActivity(userId: string) {
await cortex.users.update(userId, {
metadata: {
lastSeen: new Date(),
},
});
}
4. Privacy-First
Don't store unnecessary PII:
// ❌ Storing unnecessary data
await cortex.users.update(userId, {
metadata: {
ssn: "123-45-6789", // Don't store this!
creditCard: "4111...", // Definitely not this!
},
});
// ✅ Store only what's needed
await cortex.users.update(userId, {
preferences: {
paymentMethod: "card-ending-1234", // Reference only
},
});
5. Version Control for Important Changes
Use versioning for preference tracking:
// Enable versioning for preference changes
await cortex.users.update(
userId,
{
preferences: {
emailNotifications: false, // User opts out
},
},
{
skipVersioning: false, // Create version (default)
versionReason: "user-requested",
},
);
// Skip versioning for routine updates
await cortex.users.update(
userId,
{
metadata: {
lastSeen: new Date(),
sessionCount: user.metadata.sessionCount + 1,
},
},
{
skipVersioning: true, // Don't create version for stats
},
);
Next Steps
- Context Chains - Multi-agent coordination
- Conversation History - Message persistence
- API Reference - User API docs
Questions? Ask in GitHub Discussions or Discord.