Sign In From a Func
ctx.auth.login issues a session for an existing user — for passwordless emailed-code sign-in that the SDK cannot express, or to apply your own ban / onboarding / tenant rules at sign-in. Full code, the one rule you must not break, and where the audit trail lives.
For most sites useAuth().login() is all you need. There are exactly two reasons to implement
sign-in as a Func: passwordless login with an emailed code, which the SDK cannot express,
or server-side rules at sign-in — ban lists, required onboarding, multi-tenant checks on
which accounts may enter which tenant.
The primitive is ctx.auth.login(ref): it issues a session for an existing
user. It takes no password and no code — what makes the sign-in allowed is your code's decision.
One rule first
This goes first because it is the only way to get this capability badly wrong:
The argument to
ctx.auth.loginmust come from a fact the server just verified. Resolve the user withctx.users.findorctx.users.checkPasswordfirst, then handuser.idtologin. Writingctx.auth.login({ email: input.email })means "whoever the browser claims to be" — that is not sign-in, it lets anyone in as any account.
Why single it out: a botched password change locks the real user out, so they notice. Impersonation is silent — nobody finds out.
Passwordless: sign in with an emailed code
Requires a connected email integration, see Send email and verification codes through an integration.
// /backend/func/auth/login.ts
import type { TalizenFuncContext } from 'talizen/func-runtime'
const SCENE = 'login'
// Step 1: ask for a code. Same shape as password reset — look the user up first,
// and return the same thing either way
export function requestCode(input: { email?: string }, ctx: TalizenFuncContext) {
const email = (input.email ?? '').trim()
if (!email) throw new Error('email is required')
const user = ctx.users.find({ email })
if (user) {
try {
ctx.email.sendCode({ to: email, scene: SCENE })
} catch (err) {
console.error('send login code failed', String(err))
}
}
return { ok: true }
}
// Step 2: verify the code and sign in
export function withCode(
input: { email?: string; code?: string },
ctx: TalizenFuncContext,
) {
const email = (input.email ?? '').trim()
const code = (input.code ?? '').trim()
if (!email || !code) throw new Error('email and code are required')
const user = ctx.users.find({ email })
if (!user) throw new Error('the code is wrong or has expired')
if (!ctx.email.verifyCode({ to: email, scene: SCENE, code }))
throw new Error('the code is wrong or has expired')
// Use the id the server resolved, not input.email
return ctx.auth.login({ userId: user.id })
}
It returns the signed-in user. The session cookie is written onto that response, so a resolved call means the visitor is signed in. Func code never sees the session token and cannot mint one.
Password sign-in with your own rules
export function withPassword(
input: { account: string; password: string },
ctx: TalizenFuncContext,
) {
// Failures share the platform's /auth/login allowance: 5 wrong attempts locks
// for an hour, so this is not a way around the login lockout
if (!ctx.users.checkPassword({ account: input.account, password: input.password }))
throw new Error('wrong account or password')
const user = ctx.users.find({ account: input.account })
if (!user) throw new Error('wrong account or password')
// This is the actual reason to sign in through a Func: rules the platform
// knows nothing about
if (ctx.db.get('banned_users', user.id)) throw new Error('this account is suspended')
if (!user.profile?.onboarded) return { ok: false, next: '/onboarding' }
return ctx.auth.login({ userId: user.id })
}
After too many wrong attempts checkPassword throws 429 instead of returning
false. Give it its own message ("too many attempts, try again later"), or the visitor will keep
retrying until fully locked out.
Page code
import { invoke } from 'talizen/func'
import { useAuth } from 'talizen/auth'
async function loginWithCode(email: string, code: string) {
await invoke('auth/login.withCode', { email, code })
// The session cookie arrived with that response; refresh the hook's state
await useAuth().refresh()
window.location.href = '/account'
}
Signing out still uses useAuth().logout(); no Func needed.
Every sign-in is recorded
The platform records every session it issues: time, source, IP, device. The source is one
of Direct, OAuth (with the provider), Sign-up, or Func
(with which Func file issued it).
In the editor under Backend → Users, open a user's row menu → Login history.
This is not only a convenience feature. Sign-in through Func is the one path where site code decides who is signed in, so which file issued a session for which account has to be traceable — if the rule at the top of this page is ever broken, this is the only place it shows up.
Other things to know
| Case | Behaviour |
|---|---|
Direct /auth/login from the browser | Keeps working. There is no switch that routes login exclusively through Func; the two coexist, so you can add a Func for one entry point only |
| Unknown user | login throws 404. Do not pass that to the browser — answer with one undistinguished message like "wrong account or password" |
| Disabled account | Throws 403, matching direct login |
| The three ref fields | Pass exactly one of email / account / userId; two is a 400 |
| Accounts that only use a provider | login still issues a session (it does not care whether a password exists); checkPassword returns false for them |
| OAuth sign-in | Still handled by the platform. Do not write callbacks or token exchange in Func |
See also: Reset and Change Passwords, Require a Verified Email to Sign Up, Send email and verification codes through an integration.
