Email Verification & Password Reset

Set up outgoing email for teenybase auth, then test password reset and email verification end to end.

This guide covers:

  • the JSON auth endpoints your frontend calls
  • local testing with mock: true
  • real-provider testing after deploy

What must already exist

You need:

  • a users table with the auth extension
  • auth fields for email, password, and email verification (...authFields already gives you these)
  • an email config in teenybase.ts

1. Add email config

Minimal Resend example:

ts
export default {
  appUrl: 'https://myapp.com',
  email: {
    from: 'My App <noreply@myapp.com>',
    variables: {
      company_name: 'My App',
      company_url: 'https://myapp.com',
      company_address: '123 Main St, San Francisco, CA',
      company_copyright: 'Copyright 2026 My App',
      support_email: 'support@myapp.com',
    },
    resend: {
      RESEND_API_KEY: '$RESEND_API_KEY',
    },
  },
  tables: [
    // your tables here
  ],
} satisfies DatabaseSettings

appUrl is the base URL teenybase uses to build the reset and verification links it embeds in emails. The default templates produce:

  • {appUrl}/reset-password/{token} for password resets
  • {appUrl}/verify-email/{token} for email verification

Your frontend hosts those two routes, reads the token out of the URL, and calls the matching confirm-* JSON endpoint shown in section 4.

Then add the secret:

env
RESEND_API_KEY=your-real-provider-key

Use mailgun instead of resend if you prefer Mailgun.

Practical notes:

  • use a from address that your email provider accepts
  • on Teenybase Cloud, the first teeny deploy --remote uploads .prod.vars automatically
  • later .prod.vars changes need teeny secrets --remote --upload

2. Local test with mock email

For local development, switch the provider block to:

ts
email: {
  from: 'My App <noreply@myapp.com>',
  variables: {
    company_name: 'My App',
    company_url: 'http://localhost:8787',
    company_address: 'Local dev',
    company_copyright: 'Local dev',
    support_email: 'support@myapp.com',
  },
  mock: true,
},

Run locally:

bash
teeny deploy --local
teeny dev --local
bash
npx teeny deploy --local
npx teeny dev --local

Test password reset locally

Request the reset email:

bash
curl -X POST http://localhost:8787/api/v1/table/users/auth/request-password-reset \
  -H 'Content-Type: application/json' \
  -d '{"email":"alice@example.com"}'

Use the user's email address in the email field.

With mock: true, teenybase logs the email instead of sending it. Use the token from the logged reset link, then confirm the reset:

bash
curl -X POST http://localhost:8787/api/v1/table/users/auth/confirm-password-reset \
  -H 'Content-Type: application/json' \
  -d '{"token":"FROM_EMAIL","password":"newsecret123","passwordConfirm":"newsecret123"}'

If your auth config does not use passwordConfirmSuffix, omit passwordConfirm.

Test verification locally

First sign up or log in and keep the returned access token.

Request the verification email:

bash
curl -X POST http://localhost:8787/api/v1/table/users/auth/request-verification \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Then confirm it with the token from the mocked email:

bash
curl -X POST http://localhost:8787/api/v1/table/users/auth/confirm-verification \
  -H 'Content-Type: application/json' \
  -d '{"token":"FROM_EMAIL"}'

4. Custom frontend/API flow

Password reset

Step 1: request the email

js
await fetch(`${API}/table/users/auth/request-password-reset`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    email: 'alice@example.com',
  }),
})

Step 2: confirm the reset with the token from the email

js
await fetch(`${API}/table/users/auth/confirm-password-reset`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    token,
    password: 'newsecret123',
    passwordConfirm: 'newsecret123',
  }),
})

Email verification

Request the verification email while authenticated:

js
await fetch(`${API}/table/users/auth/request-verification`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${token}`,
  },
})

Confirm it after the user clicks the email link:

js
await fetch(`${API}/table/users/auth/confirm-verification`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ token }),
})

5. Real provider test after deploy

Once the app is deployed and .prod.vars contains the real provider key:

bash
teeny deploy --remote
teeny status
bash
npx teeny deploy --remote
npx teeny status

If you later change .prod.vars:

bash
teeny secrets --remote --upload
bash
npx teeny secrets --remote --upload

Then run this real end-to-end check:

  1. Sign up a fresh user on the deployed app.
  2. From your frontend, call request-password-reset with that user's email (or curl it directly).
  3. Open the real email and click the reset link.
  4. Your frontend's reset page reads the token from the URL and calls confirm-password-reset.
  5. Confirm the old password fails and the new password works.
  6. While authenticated, call request-verification from your frontend or the API.
  7. Open the real email and click the verification link.
  8. Your frontend's verify page calls confirm-verification and the account becomes verified.

6. What to verify in the email itself

For every real email flow, check:

  • the email arrives
  • the link opens the deployed reset or verify page successfully
  • the reset/verify action succeeds
  • the flow works end to end after the click, not just the email delivery

If links point to localhost, your appUrl or deploy setup is still wrong.

If you are on Teenybase Cloud and a link opens on an unexpected internal *.workers.dev host even though the flow still works, treat that as a platform/gateway issue rather than an appUrl mistake. Use this guide together with: