AuthSign In From a Func

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.login must come from a fact the server just verified. Resolve the user with ctx.users.find or ctx.users.checkPassword first, then hand user.id to login. Writing ctx.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

CaseBehaviour
Direct /auth/login from the browserKeeps 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 userlogin throws 404. Do not pass that to the browser — answer with one undistinguished message like "wrong account or password"
Disabled accountThrows 403, matching direct login
The three ref fieldsPass exactly one of email / account / userId; two is a 400
Accounts that only use a providerlogin still issues a session (it does not care whether a password exists); checkPassword returns false for them
OAuth sign-inStill 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.

Render diagnostics