User Profiles
User profiles enable memories to follow users across any tool. Same userId = same memories, whether accessed from Cursor, Claude, custom apps, or any MCP-connected tool. This is the foundation of Hive Mode.
Quick Start
// Create/update a user profile
await cortex.users.update("user-123", {
displayName: "Alex Johnson",
email: "alex@example.com",
preferences: { theme: "dark", language: "en" },
});
// Get user profile (works from any memory space)
const user = await cortex.users.get("user-123");
console.log(user.data.displayName); // "Alex Johnson"
Why User Profiles Exist
The primary purpose of user profiles is cross-tool memory sharing. As long as the same userId is passed to Cortex, memories follow the user regardless of which tool they're using—Cursor, Claude, custom apps, or any MCP-connected tool. This is a core enabler of Hive Mode.
// In Cursor: Store a preference
await cortex.memory.remember({
memorySpaceId: 'user-123-space',
userId: 'user-123', // Same userId
userMessage: 'I prefer TypeScript',
// ...
});
// In Claude Desktop: Recall the same memory
const memories = await cortex.memory.recall({
memorySpaceId: 'user-123-space',
userId: 'user-123', // Same userId = same memories
query: 'programming preferences',
});
// Returns: "User prefers TypeScript" - stored in Cursor, recalled in Claude!
A critical secondary benefit: One API call removes ALL user data across ALL memory layers for GDPR compliance.
// GDPR "right to be forgotten" - ONE call deletes everything
const result = await cortex.users.delete("user-123", { cascade: true });
// Deletes: profile, conversations, memories, facts, sessions, graph nodes
User Profile Schema
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
id | string | Yes | — | Unique user identifier |
tenantId | string | No | — | Multi-tenancy isolation (auto-injected from AuthContext) |
data | Record<string, unknown> | No | — | Flexible user data - any structure you want |
version | number | No | — | Auto-incrementing version number |
createdAt | number | No | — | Creation timestamp (Unix ms) |
updatedAt | number | No | — | Last update timestamp (Unix ms) |
Suggested Data Structure
await cortex.users.update("user-123", {
displayName: "Alex Johnson",
email: "alex@example.com",
preferences: {
theme: "dark",
language: "en",
timezone: "America/New_York",
},
metadata: {
tier: "pro",
signupDate: Date.now(),
},
});
User Operations
// Create or update user profile
await cortex.users.update("user-123", {
displayName: "Alex Johnson",
email: "alex@example.com",
preferences: {
theme: "dark",
language: "en",
},
metadata: {
tier: "pro",
signupDate: Date.now(),
},
});
Updates automatically deep-merge with existing data and create a new version.
// 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.version); // Version number
// Get all versions
const history = await cortex.users.getHistory("user-123");
history.forEach(v => {
console.log(`v${v.version}: ${v.data.displayName} (${new Date(v.timestamp)})`);
});
// Get profile at specific time
const historical = await cortex.users.getAtTimestamp(
"user-123",
new Date("2025-08-01")
);
// Delete user profile only
await cortex.users.delete("user-123");
// GDPR cascade - delete ALL user data
const result = await cortex.users.delete("user-123", {
cascade: true,
verify: true,
});
console.log(`Deleted ${result.totalDeleted} records`);
Cascade permanently deletes the user and ALL their data across ALL memory spaces and layers.
GDPR Compliance
Cortex supports GDPR "right to be forgotten" via cascade deletion with verification.
Preview deletion (optional)
const preview = await cortex.users.delete("user-123", {
cascade: true,
dryRun: true, // Preview only
});
console.log(`Would delete ${preview.totalDeleted} records`);
Export user data (GDPR requirement)
const exportData = await cortex.users.export({
format: "json",
filters: { displayName: "user-123" },
includeConversations: true,
includeMemories: true,
});
await saveToFile(`gdpr-export-${userId}.json`, exportData);
Execute cascade deletion
const result = await cortex.users.delete("user-123", {
cascade: true,
verify: true,
});
Automatic cleanup completes
Cortex deletes from ALL stores: User profile, all conversations (Layer 1a), all memories (Layer 2), all facts (Layer 3), all sessions, and graph nodes (if configured).
Deletion Result
{
userId: "user-123",
deletedAt: 1735689600000,
conversationsDeleted: 15,
vectorMemoriesDeleted: 145,
factsDeleted: 89,
totalDeleted: 547,
verification: { complete: true, issues: [] }
}
Automatic User Creation
User profiles are auto-created when using remember() if they don't exist:
// First call with userId creates the profile automatically
await cortex.memory.remember({
memorySpaceId: "user-123-space",
userId: "user-123", // Auto-creates profile
userName: "Alex", // Used for displayName
// ...
});
Querying Users
// Search by displayName
const users = await cortex.users.search({
displayName: "alex",
limit: 50,
});
// Search recent users
const recent = await cortex.users.search({
createdAfter: Date.now() - 30 * 24 * 60 * 60 * 1000,
sortBy: "createdAt",
sortOrder: "desc",
});
const page1 = await cortex.users.list({
limit: 50,
offset: 0,
sortBy: "createdAt",
sortOrder: "desc",
});
console.log(`${page1.users.length} of ${page1.total} users`);
const total = await cortex.users.count();
const activeThisWeek = await cortex.users.count({
updatedAfter: Date.now() - 7 * 24 * 60 * 60 * 1000,
});
// Update multiple users
await cortex.users.updateMany(
["user-1", "user-2", "user-3"],
{ data: { welcomeEmailSent: true } }
);
// Update by filter
await cortex.users.updateMany(
{ createdAfter: Date.now() - 7 * 24 * 60 * 60 * 1000 },
{ data: { welcomeEmailSent: true } }
);
Multi-Tenancy Support
Use AuthContext for automatic tenant scoping on all user operations.
import { Cortex, createAuthContext } from "@cortexmemory/sdk";
const cortex = new Cortex({
convexUrl: process.env.CONVEX_URL!,
auth: createAuthContext({
tenantId: "customer-acme",
userId: "admin-user",
}),
});
// All operations automatically scoped to tenant
await cortex.users.update("user-123", { displayName: "Alex" });
// tenantId: "customer-acme" auto-injected
const users = await cortex.users.list({ limit: 100 });
// Only returns users for "customer-acme"
await cortex.users.delete("user-123", { cascade: true });
// Only deletes within "customer-acme" tenant
See Isolation Boundaries for complete multi-tenancy documentation.
Common Patterns
Initialize on First Contact
async function ensureUserProfile(userId: string, message: string) {
let user = await cortex.users.get(userId);
if (!user) {
await cortex.users.update(userId, {
displayName: userId,
preferences: {
language: detectLanguage(message),
timezone: "UTC",
},
metadata: {
tier: "free",
signupDate: Date.now(),
},
});
user = await cortex.users.get(userId);
}
return user;
}
Cross-Agent Context
// All agents access the same profile
const user = await cortex.users.get(userId);
// Support agent
const greeting = `Hello ${user.data.displayName}!`;
// Sales agent
if (user.data.metadata.tier === "free") {
offerUpgrade();
}
// Billing agent
sendInvoiceTo(user.data.email);
Profile Validation
import { z } from "zod";
const ProfileSchema = 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(),
}).optional(),
});
async function safeUpdate(userId: string, data: unknown) {
const validated = ProfileSchema.parse(data);
return await cortex.users.update(userId, validated);
}
Best Practices
// Only require what you truly need
await cortex.users.update(userId, {
displayName: "Alex", // That's it!
});
// Add more fields over time as you learn about the user
// Don't store unnecessary PII
await cortex.users.update(userId, {
preferences: {
paymentMethod: "card-ending-1234", // Reference only
},
});
// Never store: SSN, full credit card, passwords
// Update lastSeen on each interaction
await cortex.users.update(userId, {
metadata: { lastSeen: Date.now() },
});