Custom CRUD Adapter
Credo is database-agnostic. It does not know or care how you store data. Instead, Credo communicates with your database through CRUD adapters—plain JavaScript objects containing asynchronous functions.
Your only responsibility is to implement these functions and return the expected values.
What is a CRUD Adapter?
A CRUD adapter is an object that groups database logic by domain rather than database type. Credo expects three domains:
userrefreshTokenotp
Each domain exposes a set of async functions that Credo calls internally.
Adapter Shape
const crud = {
user: {
findUserByEmail: async (email) => {},
findUserById: async (id) => {},
createUser: async (data) => {},
findUserAndUpdate: async (query, update) => {}
},
refreshToken: {
createRefreshToken: async (data) => {},
findValidRefreshTokenByTokenHash: async (tokenHash) => {},
revokeRefreshTokenByTokenHash: async (tokenHash) => {}
},
otp: {
createOTP: async (data) => {},
findOTPByEmail: async (email , purpose) => {},
deleteOTPByEmail: async (email , purpose) => {},
incrementOTPAttempts: async (email , purpose) => {},
verifyOTP: async (email, purpose) => {}
}
};
User Adapter
The user adapter handles user persistence.
Required Functions
user: {
findUserByEmail(email),
findUserById(id),
createUser(data),
findUserAndUpdate(query, update)
}
Function Contracts
findUserByEmail(email): Returns a user object ornull.findUserById(id): Returns a user object ornull.createUser(data): Creates a user and returns the created user object.findUserAndUpdate(query, update): Finds a user usingquery, updates it usingupdate, and returns the updated user ornull.
Credo does not enforce how query or update are structured; you decide how they are interpreted.
Refresh Token Adapter
Handles refresh token storage and validation.
Required Functions
refreshToken: {
createRefreshToken(data),
findValidRefreshTokenByTokenHash(tokenHash),
revokeRefreshTokenByTokenHash(tokenHash)
}
Function Contracts
createRefreshToken(data): Stores a refresh token and returns the stored token.findValidRefreshTokenByTokenHash(tokenHash): Returns a valid token object ornull.revokeRefreshTokenByTokenHash(tokenHash): Revokes the token and returnstrueorfalse.
OTP Adapter
Handles one-time password (OTP) operations.
Required Functions
otp: {
createOTP(data),
findOTPByEmail(email),
deleteOTPByEmail(email),
incrementOTPAttempts(email),
verifyOTP(email, code)
}
Function Contracts
createOTP(data): Creates an OTP entry and returns the created OTP.findOTPByEmail(email): Returns OTP data ornull.deleteOTPByEmail(email): Deletes OTP records and returnstrueorfalse.incrementOTPAttempts(email): Increments failed attempts and returns updated OTP data.verifyOTP(email, code): Verifies the OTP and returnstrueorfalse.
Example: Simple In-Memory Adapter
This example demonstrates how adapters work without an external database.
const users = [];
export const userAdapter = {
findUserByEmail: async (email) => {
return users.find(u => u.email === email) || null;
},
findUserById: async (id) => {
return users.find(u => u.id === id) || null;
},
createUser: async (data) => {
const user = { id: Date.now().toString(), ...data };
users.push(user);
return user;
},
updateUserPassword: async (email, password) => {
const user = users.find(u => u.email === email);
if (!user) return null;
user.password = password;
return user;
},
verfiyUserEmail: async (email) => {
const user = users.find(u => u.email === email);
if (!user) return null;
user.isEmailVerified = true;
return user;
}
};
const refreshTokens = []
export const refreshTokenAdapter = {
createRefreshToken: async (data) => {
const token = {
id: Date.now(),
revoked: false,
...data
}
refreshTokens.push(token)
return token
},
findValidRefreshTokenByTokenHash: async (tokenHash) => {
return (
refreshTokens.find(
t => t.tokenHash === tokenHash && t.revoked === false
) || null
)
},
revokeRefreshTokenByTokenHash: async (tokenHash) => {
const token = refreshTokens.find(t => t.tokenHash === tokenHash)
if (!token) return false
token.revoked = true
return true
}
}
const otps = []
export const otpAdapter = {
createOTP: async (data) => {
const otp = {
attempts: 0,
verified: false,
createdAt: Date.now(),
...data
}
otps.push(otp)
return otp
},
findOTPByEmail: async (email , purpose) => {
return otps.find(o => o.email === email && o.purpose === purpose) || null
},
deleteOTPByEmail: async (email , purpose) => {
const index = otps.findIndex(o => o.email === email && o.purpose === purpose)
if (index === -1) return false
otps.splice(index, 1)
return true
},
incrementOTPAttempts: async (email , purpose) => {
const otp = otps.find(o => o.email === email && o.purpose === purpose)
if (!otp) return null
otp.attempts += 1
return otp
},
verifyOTP: async (email, code , purpose) => {
const otp = otps.find(o => o.email === email && o.purpose === purpose)
if (!otp) return false
if (otp.code !== code) {
otp.attempts += 1
return false
}
otp.verified = true
return true
}
}
This same pattern applies to any database. Whether using MongoDB, SQL, or Prisma:
- SQL users may map updates manually.
- Prisma users may ignore
queryand map IDs. - All Credo cares about is the returned value.
Plugging the Adapter into Credo
createAuthSystem({
crud: {
user: userAdapter,
refreshToken: refreshTokenAdapter,
otp: otpAdapter
}
});
Once provided, Credo will use your adapters internally for all authentication flows.