Skip to content

Instantly share code, notes, and snippets.

@ahmedrowaihi
Last active May 17, 2025 15:27
Show Gist options
  • Save ahmedrowaihi/715f3b43d786d4bd200c6f932848cb58 to your computer and use it in GitHub Desktop.
Save ahmedrowaihi/715f3b43d786d4bd200c6f932848cb58 to your computer and use it in GitHub Desktop.
// incomplete - better-auth letta identity based adapter
import type { LettaClient } from "@letta-ai/letta-client";
import { Identity, IdentityProperty } from "@letta-ai/letta-client/api";
import { generateId } from "better-auth";
import type {
Account,
Adapter,
BetterAuthOptions,
Session,
User,
Verification,
Where,
} from "better-auth/types";
const getIdentity = async (
letta: LettaClient,
where: Where[],
type: string = "email"
): Promise<Identity | undefined> => {
const value = where.find((clause) => clause.field === type)?.value;
if (!value) return undefined;
if (type === "email") {
return letta.identities
.list({
identityType: "user",
limit: 1,
identifierKey: String(value),
})
.then((results) => results[0]);
}
if (type === "userId") {
return letta.identities.retrieve(String(value));
}
return undefined;
};
const updateIdentityProperties = async (
letta: LettaClient,
identityId: string,
key: string,
value: any
): Promise<void> => {
await letta.identities.modify(identityId, {
properties: [
{
key,
value:
typeof value === "object" ? JSON.stringify(value) : String(value),
type:
typeof value === "object"
? "json"
: typeof value === "boolean"
? "boolean"
: "string",
},
],
});
};
const transformers = {
user: {
transform: (identity: Identity): User => ({
id: identity.id ?? "",
email: parseIdentityProperty<string>(identity, "email") ?? "",
name: parseIdentityProperty<string>(identity, "name") ?? "",
image: parseIdentityProperty<string | null>(identity, "image") ?? null,
createdAt: new Date(
parseIdentityProperty<string>(identity, "createdAt") ??
new Date().toISOString()
),
updatedAt: new Date(
parseIdentityProperty<string>(identity, "updatedAt") ??
new Date().toISOString()
),
emailVerified:
parseIdentityProperty<boolean>(identity, "emailVerified") ?? false,
}),
parse: (identity: Identity) => [identity],
update: async (
letta: LettaClient,
identity: Identity,
data: Partial<User>
) => {
if (!identity.id) throw new Error("Identity not found");
await updateIdentityProperties(letta, identity.id, "user", data);
},
},
account: {
transform: (account: Account, userId: string): Account => ({
id: account.id,
userId,
providerId: account.providerId,
accountId: account.accountId,
password: account.password,
createdAt: account.createdAt,
updatedAt: account.updatedAt,
}),
parse: (identity: Identity) =>
parseIdentityProperty<Account[]>(identity, "accounts") ?? [],
update: async (
letta: LettaClient,
identity: Identity,
accounts: Account[]
) => {
if (!identity.id) throw new Error("Identity not found");
await updateIdentityProperties(letta, identity.id, "accounts", accounts);
},
},
session: {
transform: (session: Session, userId: string): Session => ({
id: session.id,
userId,
expiresAt: session.expiresAt,
token: session.token,
ipAddress: session.ipAddress,
userAgent: session.userAgent,
createdAt: session.createdAt,
updatedAt: session.updatedAt,
}),
parse: (identity: Identity) =>
parseIdentityProperty<Session[]>(identity, "sessions") ?? [],
update: async (
letta: LettaClient,
identity: Identity,
sessions: Session[]
) => {
if (!identity.id) throw new Error("Identity not found");
await updateIdentityProperties(letta, identity.id, "sessions", sessions);
},
},
verification: {
transform: (verification: Verification): Verification => ({
id: verification.id,
identifier: verification.identifier,
value: verification.value,
expiresAt: verification.expiresAt,
createdAt: verification.createdAt,
updatedAt: verification.updatedAt,
}),
parse: (identity: Identity) =>
parseIdentityProperty<Verification[]>(identity, "verification") ?? [],
update: async (
letta: LettaClient,
identity: Identity,
verifications: Verification[]
) => {
if (!identity.id) throw new Error("Identity not found");
await updateIdentityProperties(
letta,
identity.id,
"verification",
verifications
);
},
},
};
const parseIdentityProperty = <T>(
identity: Identity,
key: string
): T | undefined => {
const property = identity.properties?.find((p) => p.key === key);
try {
if (!property?.value && property?.key !== "image") return undefined;
if (property?.type === "json") {
return JSON.parse(property.value as string) as T;
}
return property.value as T;
} catch (e) {
return undefined;
}
};
const generateInitialProperties = (
data: Omit<User, "id">
): IdentityProperty[] => [
{ key: "email", value: data.email, type: "string" },
{ key: "name", value: data.name || data.email, type: "string" },
{ key: "image", value: data.image || "", type: "string" },
{ key: "emailVerified", value: data.emailVerified, type: "boolean" },
{ key: "createdAt", value: data.createdAt.toISOString(), type: "string" },
{ key: "updatedAt", value: data.updatedAt.toISOString(), type: "string" },
{ key: "accounts", value: JSON.stringify([]), type: "json" },
{ key: "sessions", value: JSON.stringify([]), type: "json" },
{ key: "verification", value: JSON.stringify([]), type: "json" },
];
type AuthModel = "user" | "account" | "session" | "verification";
const isValidModel = (model: string): model is AuthModel => {
return ["user", "account", "session", "verification"].includes(model);
};
export const lettaAdapter =
(letta: LettaClient) => (options: BetterAuthOptions) => {
const debug = (message: string, ...args: any[]) => {
if (options.logger?.log) {
options.logger.log("info", message, ...args);
} else {
console.log(message, ...args);
}
};
if (!letta) {
throw new Error("Letta adapter requires a Letta client");
}
const genId = () =>
options.advanced?.generateId
? options.advanced.generateId({ model: "user" })
: generateId();
const createUserIdentity = async (
data: Omit<User, "id">
): Promise<User> => {
const identity = await letta.identities.create({
identifierKey: data.email,
name: data.name || data.email,
identityType: "user",
properties: generateInitialProperties(data),
});
return transformers.user.transform(identity);
};
const createAccount = async (
userId: string,
data: Omit<Account, "id">
): Promise<Account> => {
debug("[LettaAdapter] Creating account with userId:", userId);
if (!userId) {
throw new Error("userId is required");
}
try {
const identity = await letta.identities.retrieve(userId);
debug("[LettaAdapter] Retrieved identity:", { id: identity?.id });
if (!identity || !identity.id) {
debug("[LettaAdapter] Identity not found for userId:", userId);
throw new Error("Identity not found");
}
const identityProperties = identity.properties;
if (!identityProperties) {
debug(
"[LettaAdapter] Identity properties not found for:",
identity.id
);
throw new Error("Identity properties not found");
}
const accounts = transformers.account.parse(identity) || [];
debug("[LettaAdapter] Existing accounts:", accounts.length);
const account: Account = {
id: genId(),
...data,
userId: identity.id,
createdAt: new Date(),
updatedAt: new Date(),
};
debug("[LettaAdapter] Created new account:", { id: account.id });
const existingAccountIndex = accounts.findIndex(
(a) =>
a.providerId === account.providerId &&
a.accountId === account.accountId
);
if (existingAccountIndex >= 0) {
accounts[existingAccountIndex] = account;
debug("[LettaAdapter] Updated existing account");
} else {
accounts.push(account);
debug("[LettaAdapter] Added new account");
}
await transformers.account.update(letta, identity, accounts);
debug("[LettaAdapter] Updated identity with new accounts");
return account;
} catch (error) {
debug("[LettaAdapter] Error creating account:", error);
throw error;
}
};
const createVerification = async (
userId: string,
data: Omit<Verification, "id">
): Promise<Verification> => {
if (!userId) {
throw new Error("userId is required");
}
const identity = await getIdentity(letta, [
{ field: "userId", value: userId },
]);
if (!identity || !identity.id) {
throw new Error("Identity not found");
}
const identityProperties = identity.properties;
if (!identityProperties) {
throw new Error("Identity properties not found");
}
const verifications = transformers.verification.parse(identity) || [];
const verification: Verification = {
id: genId(),
value: data.value,
identifier: data.identifier,
expiresAt: data.expiresAt,
createdAt: new Date(),
updatedAt: new Date(),
};
const existingVerificationIndex = verifications.findIndex(
(v) => v.identifier === verification.identifier
);
if (existingVerificationIndex >= 0) {
verifications[existingVerificationIndex] = verification;
} else {
verifications.push(verification);
}
await transformers.verification.update(letta, identity, verifications);
return verification;
};
const createSession = async (
userId: string,
data: Omit<Session, "id">
): Promise<Session> => {
debug("[LettaAdapter] Creating session with userId:", userId);
if (!userId) {
throw new Error("userId is required");
}
try {
const identity = await letta.identities.retrieve(userId);
debug("[LettaAdapter] Retrieved identity:", { id: identity?.id });
if (!identity || !identity.id) {
debug("[LettaAdapter] Identity not found for userId:", userId);
throw new Error("Identity not found");
}
const identityProperties = identity.properties;
if (!identityProperties) {
debug(
"[LettaAdapter] Identity properties not found for:",
identity.id
);
throw new Error("Identity properties not found");
}
const sessions = transformers.session.parse(identity) || [];
debug("[LettaAdapter] Existing sessions:", sessions.length);
const ip =
data.ipAddress ||
(data as any).headers?.["x-forwarded-for"]?.split(",")[0]?.trim() ||
(data as any).headers?.["x-real-ip"] ||
"";
const session: Session = {
id: genId(),
token: data.token,
userId: identity.id,
expiresAt: data.expiresAt,
ipAddress: ip,
userAgent: data.userAgent,
createdAt: new Date(),
updatedAt: new Date(),
};
debug("[LettaAdapter] Created new session:", { id: session.id });
const existingSessionIndex = sessions.findIndex(
(s) => s.token === session.token
);
if (existingSessionIndex >= 0) {
sessions[existingSessionIndex] = session;
debug("[LettaAdapter] Updated existing session");
} else {
sessions.push(session);
debug("[LettaAdapter] Added new session");
}
await transformers.session.update(letta, identity, sessions);
debug("[LettaAdapter] Updated identity with new sessions");
return session;
} catch (error) {
debug("[LettaAdapter] Error creating session:", error);
throw error;
}
};
return {
id: "letta",
create: async <T extends Record<string, any>, R = T>({
model,
data,
select,
}: {
model: string;
data: Omit<T, "id">;
select?: string[];
}): Promise<R> => {
debug(`[LettaAdapter] create called for model: ${model}`, {
data,
select,
});
if (!isValidModel(model)) {
debug(`[LettaAdapter] Invalid model: ${model}`);
throw new Error(`Invalid model: ${model}`);
}
try {
if (model === "user") {
const user = await createUserIdentity(
data as unknown as Omit<User, "id">
);
debug(`[LettaAdapter] User created:`, { id: user.id });
return { id: user.id } as R;
} else if (model === "account") {
const accountData = {
...data,
createdAt: new Date(),
updatedAt: new Date(),
providerId: (data as any).providerId,
accountId: (data as any).accountId,
userId: (data as any).userId,
} as Omit<Account, "id">;
if (!accountData.userId) {
debug(`[LettaAdapter] userId is required for account creation`);
throw new Error("userId is required for account creation");
}
const account = await createAccount(
accountData.userId,
accountData
);
debug(`[LettaAdapter] Account created:`, { id: account.id });
return { id: account.id } as R;
} else if (model === "verification") {
const verificationData = {
value: (data as any).value,
identifier: (data as any).identifier,
expiresAt: (data as any).expiresAt,
createdAt: new Date(),
updatedAt: new Date(),
} as Omit<Verification, "id">;
if (!verificationData.identifier) {
debug(
`[LettaAdapter] identifier is required for verification creation`
);
throw new Error(
"identifier is required for verification creation"
);
}
const verification = await createVerification(
(data as any).userId,
verificationData
);
debug(`[LettaAdapter] Verification created:`, {
id: verification.id,
});
return { id: verification.id } as R;
} else if (model === "session") {
const sessionData = {
token: (data as any).token,
userId: (data as any).userId,
expiresAt: (data as any).expiresAt,
ipAddress: (data as any).ipAddress,
userAgent: (data as any).userAgent,
createdAt: new Date(),
updatedAt: new Date(),
} as Omit<Session, "id">;
if (!sessionData.userId) {
debug(`[LettaAdapter] userId is required for session creation`);
throw new Error("userId is required for session creation");
}
const session = await createSession(
sessionData.userId,
sessionData
);
debug(`[LettaAdapter] Session created:`, { id: session.id });
return { id: session.id } as R;
}
return { id: genId() } as R;
} catch (error) {
debug(`[LettaAdapter] Error in create for model ${model}:`, error);
throw error;
}
},
findOne: async <T>({
model,
where,
select = [],
}: {
model: string;
where: Where[];
select?: string[];
}): Promise<T | null> => {
debug(`[LettaAdapter] findOne called for model: ${model}`, {
where,
select,
});
if (!isValidModel(model)) {
debug(`[LettaAdapter] Invalid model: ${model}`);
throw new Error(`Invalid model: ${model}`);
}
try {
if (model === "user") {
const emailClause = where.find(
(clause: Where) => clause.field === "email"
);
if (!emailClause?.value) {
debug(`[LettaAdapter] No email provided for user lookup`);
return null;
}
const identity = await getIdentity(letta, where, "email");
if (!identity) {
debug(
`[LettaAdapter] No user found for email: ${emailClause.value}`
);
return null;
}
const user = transformers.user.transform(identity);
debug(`[LettaAdapter] User found:`, { id: user.id });
return user as T;
}
if (model === "account") {
const userIdClause = where.find(
(clause: Where) => clause.field === "userId"
);
if (!userIdClause?.value) {
debug(`[LettaAdapter] No userId provided for account lookup`);
return null;
}
const identity = await getIdentity(letta, where, "userId");
if (!identity) {
debug(
`[LettaAdapter] No identity found for userId: ${userIdClause.value}`
);
return null;
}
const accounts = transformers.account.parse(identity) || [];
const accountIdClause = where.find(
(clause: Where) => clause.field === "accountId"
);
if (accountIdClause?.value) {
const account = accounts.find(
(acc) => acc.accountId === accountIdClause.value
);
if (!account) return null;
debug(`[LettaAdapter] Account found:`, { id: account.id });
return transformers.account.transform(account, identity.id!) as T;
}
const firstAccount = accounts[0];
if (!firstAccount) return null;
debug(`[LettaAdapter] First account found:`, {
id: firstAccount.id,
});
return transformers.account.transform(
firstAccount,
identity.id!
) as T;
}
if (model === "session") {
const userIdClause = where.find(
(clause: Where) => clause.field === "userId"
);
if (!userIdClause?.value) {
debug(`[LettaAdapter] No userId provided for session lookup`);
return null;
}
const identity = await getIdentity(letta, where, "id");
if (!identity) {
debug(
`[LettaAdapter] No identity found for userId: ${userIdClause.value}`
);
return null;
}
const sessions = transformers.session.parse(identity) || [];
const sessionTokenClause = where.find(
(clause: Where) => clause.field === "token"
);
if (sessionTokenClause?.value) {
const session = sessions.find(
(s) => s.token === sessionTokenClause.value
);
if (!session) return null;
debug(`[LettaAdapter] Session found:`, { id: session.id });
return transformers.session.transform(session, identity.id!) as T;
}
const firstSession = sessions[0];
if (!firstSession) return null;
debug(`[LettaAdapter] First session found:`, {
id: firstSession.id,
});
return transformers.session.transform(
firstSession,
identity.id!
) as T;
}
if (model === "verification") {
const identifierClause = where.find(
(clause: Where) => clause.field === "identifier"
);
if (!identifierClause?.value) {
debug(
`[LettaAdapter] No identifier provided for verification lookup`
);
return null;
}
const identity = await getIdentity(letta, where, "id");
if (!identity) {
debug(
`[LettaAdapter] No identity found for identifier: ${identifierClause.value}`
);
return null;
}
const verifications =
transformers.verification.parse(identity) || [];
const verification = verifications.find(
(v) => v.identifier === identifierClause.value
);
if (!verification) return null;
debug(`[LettaAdapter] Verification found:`, {
id: verification.id,
});
return transformers.verification.transform(verification) as T;
}
return null;
} catch (error) {
debug(`[LettaAdapter] Error in findOne for model ${model}:`, error);
throw error;
}
},
findMany: async <T>({
model,
where = [],
sortBy,
limit,
offset,
}: {
model: string;
where?: Where[];
sortBy?: { field: string; direction: "asc" | "desc" };
limit?: number;
offset?: number;
}): Promise<T[]> => {
debug(`[LettaAdapter] findMany called for model: ${model}`, {
where,
sortBy,
limit,
offset,
});
if (!isValidModel(model)) {
debug(`[LettaAdapter] Invalid model: ${model}`);
throw new Error(`Invalid model: ${model}`);
}
try {
if (model === "user") {
const results = await letta.identities.list({
identityType: "user",
limit,
before: offset ? offset.toString() : undefined,
});
const users = results.map((identity) =>
transformers.user.transform(identity)
);
debug(`[LettaAdapter] Found ${users.length} users`);
return users as T[];
} else if (model === "account") {
const userIdClause = where.find(
(clause: Where) => clause.field === "userId"
);
if (!userIdClause?.value) {
debug(`[LettaAdapter] No userId provided for account lookup`);
return [];
}
const identity = await getIdentity(
letta,
where,
userIdClause.field
);
if (!identity) return [];
const accounts = transformers.account.parse(identity) || [];
debug(`[LettaAdapter] Found ${accounts.length} accounts`);
const mappedAccounts = accounts.map((account) =>
transformers.account.transform(account, identity.id!)
);
return mappedAccounts as T[];
} else if (model === "session") {
const userIdClause = where.find(
(clause: Where) => clause.field === "userId"
);
if (!userIdClause?.value) {
debug(`[LettaAdapter] No userId provided for session lookup`);
return [];
}
const identity = await getIdentity(
letta,
where,
userIdClause.field
);
if (!identity) return [];
const sessions = transformers.session.parse(identity) || [];
debug(`[LettaAdapter] Found ${sessions.length} sessions`);
const mappedSessions = sessions.map((session) =>
transformers.session.transform(session, identity.id!)
);
return mappedSessions as T[];
} else if (model === "verification") {
const identifierClause = where.find(
(clause: Where) => clause.field === "identifier"
);
if (!identifierClause?.value) {
debug(
`[LettaAdapter] No identifier provided for verification lookup`
);
return [];
}
const identity = await getIdentity(
letta,
where,
identifierClause.field
);
if (!identity) return [];
const verifications =
transformers.verification.parse(identity) || [];
debug(`[LettaAdapter] Found ${verifications.length} verifications`);
const mappedVerifications = verifications.map((verification) =>
transformers.verification.transform(verification)
);
return mappedVerifications as T[];
}
debug(`[LettaAdapter] No model found for ${model}`);
return [];
} catch (error) {
debug(`[LettaAdapter] Error in findMany for model ${model}:`, error);
throw error;
}
},
count: async ({
model,
where = [],
}: {
model: string;
where?: Where[];
}): Promise<number> => {
debug(`[LettaAdapter] count called for model: ${model}`, {
where,
});
if (!isValidModel(model)) {
debug(`[LettaAdapter] Invalid model: ${model}`);
throw new Error(`Invalid model: ${model}`);
}
try {
if (model === "user") {
const results = await letta.identities.list({
identityType: "user",
});
debug(`[LettaAdapter] Counted ${results.length} users`);
return results.length;
}
return 0;
} catch (error) {
debug(`[LettaAdapter] Error in count for model ${model}:`, error);
throw error;
}
},
update: async <T>({
model,
where,
update,
}: {
model: string;
where: Where[];
update: Record<string, any>;
}): Promise<T | null> => {
debug(`[LettaAdapter] update called for model: ${model}`, {
where,
update,
});
if (!isValidModel(model)) {
debug(`[LettaAdapter] Invalid model: ${model}`);
throw new Error(`Invalid model: ${model}`);
}
try {
if (model === "user") {
const emailClause = where.find(
(clause: Where) => clause.field === "email"
);
if (!emailClause?.value) {
debug(`[LettaAdapter] No email provided for update`);
throw new Error("No email provided for update");
}
const identity = await getIdentity(letta, where, emailClause.field);
if (!identity) {
debug(
`[LettaAdapter] No user found for email: ${emailClause.value}`
);
throw new Error("User not found");
}
await transformers.user.update(
letta,
identity,
update as Partial<User>
);
const user = transformers.user.transform(identity);
debug(`[LettaAdapter] User updated:`, { id: user.id });
return user as T;
}
throw new Error(`Update not implemented for model: ${model}`);
} catch (error) {
debug(`[LettaAdapter] Error in update for model ${model}:`, error);
throw error;
}
},
delete: async <T>({
model,
where,
}: {
model: string;
where: Where[];
}): Promise<void> => {
debug(`[LettaAdapter] delete called for model: ${model}`, {
where,
});
if (!isValidModel(model)) {
debug(`[LettaAdapter] Invalid model: ${model}`);
throw new Error(`Invalid model: ${model}`);
}
try {
if (model === "user") {
const emailClause = where.find(
(clause: Where) => clause.field === "email"
);
if (!emailClause?.value) {
debug(`[LettaAdapter] No email provided for delete`);
throw new Error("No email provided for delete");
}
const identity = await getIdentity(letta, where, emailClause.field);
if (!identity) {
debug(
`[LettaAdapter] No user found for email: ${emailClause.value}`
);
throw new Error("User not found");
}
await letta.identities.delete(identity.id!);
debug(`[LettaAdapter] User deleted:`, {
email: emailClause.value,
});
return;
}
throw new Error(`Delete not implemented for model: ${model}`);
} catch (error) {
debug(`[LettaAdapter] Error in delete for model ${model}:`, error);
throw error;
}
},
deleteMany: async ({
model,
where = [],
}: {
model: string;
where: Where[];
}): Promise<number> => {
debug(`[LettaAdapter] deleteMany called for model: ${model}`, {
where,
});
if (!isValidModel(model)) {
debug(`[LettaAdapter] Invalid model: ${model}`);
throw new Error(`Invalid model: ${model}`);
}
try {
if (model === "user") {
const results = await letta.identities.list({
identityType: "user",
});
const count = results.length;
for (const result of results) {
if (!result.id) continue;
await letta.identities.delete(result.id);
}
debug(`[LettaAdapter] Deleted ${count} users`);
return count;
}
return 0;
} catch (error) {
debug(
`[LettaAdapter] Error in deleteMany for model ${model}:`,
error
);
throw error;
}
},
updateMany: async ({
model,
where,
update,
}: {
model: string;
where: Where[];
update: Record<string, any>;
}): Promise<number> => {
debug(`[LettaAdapter] updateMany called for model: ${model}`, {
where,
update,
});
if (!isValidModel(model)) {
debug(`[LettaAdapter] Invalid model: ${model}`);
throw new Error(`Invalid model: ${model}`);
}
try {
if (model === "user") {
const results = await letta.identities.list({
identityType: "user",
});
let count = 0;
for (const result of results) {
if (!result.id) continue;
await transformers.user.update(
letta,
result,
update as Partial<User>
);
count++;
}
debug(`[LettaAdapter] Updated ${count} users`);
return count;
}
return 0;
} catch (error) {
debug(
`[LettaAdapter] Error in updateMany for model ${model}:`,
error
);
throw error;
}
},
} satisfies Adapter;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment