Getting Started
1. Add Auth
Section titled “1. Add Auth”Create an Auth module and register it with the backend app.
import { backend } from "@layeron/core"import { auth } from "@layeron/modules"
const app = backend()const appAuth = auth({ session: { mode: "both", accessTokenTtl: "15m", refreshTokenTtl: "30d", refreshTokenRotation: true, },})
app.use(appAuth)The default database mode is managed. Auth creates its own internal Database
Product store for users and sessions.
2. Add Email Sign-Up
Section titled “2. Add Email Sign-Up”Email sign-up uses an Email product to send the sign-up message. Configure the Email product first, then pass it to Auth.
import { auth, email } from "@layeron/modules"
const mail = email.send({ name: "main", namespace: "mailer", domain: "example.com",})const appAuth = auth({ email: { product: mail, sender: "auth@example.com", template: "auth-sign-up", otp: { required: true, length: 6, ttl: "10m", minSendInterval: "1m", maxAttempts: 5, }, }, password: { minLength: 12, requireNumber: true, hash: { algorithm: "argon2id", level: "balanced", argon2id: { memoryKiB: 32768, iterations: 2, }, }, }, signInWithPassword: { emailOtp: { required: true, template: "auth-sign-in", ttl: "10m", minSendInterval: "1m", remember: { enabled: true, ttl: "30d", }, }, }, updatePassword: { requireCurrentPassword: true, emailOtp: { required: true, template: "auth-password-update", ttl: "10m", minSendInterval: "1m", maxAttempts: 5, }, }, resetPasswordForEmail: { template: "auth-password-reset", minSendInterval: "1m", token: { ttl: "10m", maxAttempts: 5, }, },})
app.use(mail)app.use(appAuth)Auth sends email through the Email product send API. The configured template
name, OTP or token data, metadata, and idempotency key are passed in the email
payload.
Auth uses minSendInterval to limit repeat email sends for the same active
OTP or reset flow. The default is 1m. During that interval, Auth reuses the
active challenge and skips another Email product call.
password.minLength must be at least 8. Auth rejects weaker configuration at
startup and rejects weak passwords during sign-up. Password hashes use Argon2id
by default with 32768 KiB of memory, 2 iterations, 1 parallelism, a
16-byte salt, and a 32-byte derived hash.
Set password.enabled to false only when the app uses another login method
for all password-related flows. Auth then rejects password sign-up, password
sign-in, password updates, and password reset requests.
Use PBKDF2-SHA256 when an application needs the older SHA-256 based hash format. Auth runs PBKDF2 through Workers Web Crypto.
const appAuth = auth({ password: { hash: { algorithm: "pbkdf2_sha256", pbkdf2Sha256: { iterations: 600000, }, }, },})When email.otp.required is true, signUp creates the user and password
credential, creates an email_password identity, stores a hashed OTP token,
sends the OTP through Email, and returns without a session.
app.post("/signup", async (request) => { const body = await request.json() const result = await appAuth.signUp({ email: body.email, password: body.password, user: { displayName: body.name, }, })
return { user: result.user, verification: result.verification, }})Complete OTP sign-up by verifying the code. Auth marks the email as verified, creates a session, and returns the session token.
app.post("/signup/verify", async (request) => { const body = await request.json() const result = await appAuth.verifyEmailOtp({ email: body.email, verificationId: body.verificationId, otp: body.otp, })
return { user: result.user, accessToken: result.accessToken, }})Set email.otp.required to false when the app should create a session during
sign-up. Auth still sends the configured sign-up email through the Email
product.
3. Sign In With Password
Section titled “3. Sign In With Password”Call signInWithPassword after collecting the user’s email and password.
app.post("/login", async (request) => { const body = await request.json() const result = await appAuth.signInWithPassword({ email: body.email, password: body.password, rememberToken: body.rememberToken, })
return result})When signInWithPassword.emailOtp.required is true, Auth verifies the
password, stores a hashed OTP token, sends the OTP through Email, and returns
without a session. A valid rememberToken skips the OTP step for the same
email and creates the session after the password check.
Repeat sign-in attempts inside signInWithPassword.emailOtp.minSendInterval
reuse the active OTP challenge.
app.post("/login/verify", async (request) => { const body = await request.json() const result = await appAuth.verifySignInEmailOtp({ email: body.email, verificationId: body.verificationId, otp: body.otp, remember: body.rememberDevice, })
return { user: result.user, accessToken: result.accessToken, rememberToken: result.rememberToken, rememberTokenExpiresAt: result.rememberTokenExpiresAt, }})Auth stores remember tokens as hashes and returns the plain token only from the
successful verifySignInEmailOtp call. remember: true requires
signInWithPassword.emailOtp.remember.enabled.
4. Update A Password
Section titled “4. Update A Password”updatePassword requires an active session. By default, Auth also requires the
current password. Set updatePassword.requireCurrentPassword to false when a
logged-in user can set a new password directly.
app.post("/password", { auth: "user" }, async (request) => { const body = await request.json() const result = await appAuth.updatePassword({ currentPassword: body.currentPassword, newPassword: body.newPassword, })
return result})When updatePassword.emailOtp.required is true, Auth verifies the session
and current password policy, stores a pending password hash, sends the OTP
through Email, and returns without changing the credential.
Repeat password update attempts inside updatePassword.emailOtp.minSendInterval
reuse the active OTP challenge.
app.post("/password/verify", { auth: "user" }, async (request) => { const body = await request.json() const result = await appAuth.verifyPasswordUpdateEmailOtp({ verificationId: body.verificationId, otp: body.otp, })
return { user: result.user, updatedAt: result.updatedAt, }})Auth stores the pending password as a hash while the OTP is active. A successful OTP verification writes the new password hash to the credential record.
5. Reset A Password By Email
Section titled “5. Reset A Password By Email”resetPasswordForEmail sends a one-time reset token through the Email product.
Auth stores only the token hash. The method returns the same sent: true
result when the email has no password credential.
Repeat reset requests inside resetPasswordForEmail.minSendInterval reuse the
active reset flow and return the same neutral result.
app.post("/password/reset", async (request) => { const body = await request.json()
return await appAuth.resetPasswordForEmail({ email: body.email, })})Use verifyPasswordResetToken before showing a password reset form when the
application needs a separate token check.
app.post("/password/reset/verify", async (request) => { const body = await request.json()
return await appAuth.verifyPasswordResetToken({ verificationId: body.verificationId, token: body.token, })})Use setPasswordWithResetToken to write the new password. The new password
uses the same password strength policy as sign-up and logged-in password
updates.
app.post("/password/reset/complete", async (request) => { const body = await request.json()
return await appAuth.setPasswordWithResetToken({ verificationId: body.verificationId, token: body.token, newPassword: body.newPassword, })})6. Create A Session Manually
Section titled “6. Create A Session Manually”Call createSession after your application has validated a login attempt.
app.post("/login", async () => { const result = await appAuth.createSession({ user: { email: "ada@example.com", displayName: "Ada", roles: ["member"], }, }) const headers = new Headers({ "content-type": "application/json", })
if (result.setCookie) { headers.set("set-cookie", result.setCookie) }
return new Response(JSON.stringify({ user: result.user }), { headers, })})In managed mode, Auth creates or updates the user row before it writes the
session row. In mapped, custom, and external modes, pass userId or
user.id so Auth can resolve the user before it writes the session row.
7. Refresh A Session
Section titled “7. Refresh A Session”createSession, sign-up, password sign-in, OTP verification, and password reset
completion can return a refreshToken. Store the refresh token in server-side
session storage or in a secure client storage strategy chosen by the
application.
app.post("/session/refresh", async (request) => { const body = await request.json() const result = await appAuth.refreshSession({ refreshToken: body.refreshToken, })
return { accessToken: result.accessToken, refreshToken: result.refreshToken, session: result.session, }})By default, refreshSession rotates the refresh token and marks the old token
as replaced. Reusing a replaced, revoked, expired, or reused token fails with
auth_invalid_refresh_token and revokes the token family.
8. Protect A Route
Section titled “8. Protect A Route”Use auth: "user" on any route that requires a valid session.
app.get("/me", { auth: "user" }, async () => { const user = await appAuth.getUser()
return user})The Gateway reads the bearer token or session cookie, verifies it through the
Auth Product Worker, and returns 401 when the token is missing, expired, or
revoked.
9. Read And Revoke Sessions
Section titled “9. Read And Revoke Sessions”Read the active user and session from the current request token.
app.get("/me", { auth: "user" }, async () => { return { user: await appAuth.getUser(), userId: await appAuth.getUserId(), session: await appAuth.getSession(), }})Use signOut to revoke the active session. revokeSession accepts a session id
or access token. revokeAllSessions revokes every active session for the
current user, or for a specific userId in server-side code.
app.post("/logout", { auth: "user" }, async () => { const session = await appAuth.signOut()
return { revoked: Boolean(session?.revokedAt), }})app.post("/logout-all", { auth: "user" }, async () => { return await appAuth.revokeAllSessions()})10. Handle Auth Errors
Section titled “10. Handle Auth Errors”Auth methods throw AuthError. The error has code, message, status, and
details fields. Route error responses use the same code and status when an
Auth error reaches the Gateway.
import { AuthError } from "@layeron/modules"
app.post("/password", { auth: "user" }, async (request) => { try { const body = await request.json() return await appAuth.updatePassword({ currentPassword: body.currentPassword, newPassword: body.newPassword, }) } catch (error) { if (error instanceof AuthError) { return Response.json(error.toJSON(), { status: error.status }) }
throw error }})Next Steps
Section titled “Next Steps”- User storage modes: Choose where Auth reads and writes user records.
- GitHub login: Add built-in OAuth sign-in with Auth-managed sessions.
- OIDC login: Connect an OpenID Connect provider with PKCE.
- Passkeys: Add WebAuthn registration, login, MFA, and step-up checks.
- API reference: Review Auth options, operations, and runtime result contracts.