Skip to main content

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() calls cortex.immutable.store() with type='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 userId field.

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 field
  • createdBefore/After - Date range for creation
  • updatedBefore/After - Date range for updates
  • email - 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


Questions? Ask in GitHub Discussions or Discord.