---
title: "Email Verification & Password Reset"
description: "Set up outgoing email for teenybase auth and test password reset and email verification end to end, locally with mock email and after deploy."
url: https://teenybase.com/docs/guides/email-flows
last_updated: 2026-10-26
---

> Documentation index: https://teenybase.com/llms.txt lists every teenybase docs page with a link to its Markdown version. All docs in one file: https://teenybase.com/llms-full.txt

# 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
- the first `npx teenybase deploy --remote` uploads `.prod.vars` automatically (generating any missing secrets)
- later `.prod.vars` changes need `npx teenybase 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
npx teenybase deploy --local
npx teenybase dev
```

### 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
npx teenybase deploy --remote --yes
```

The deploy prints your production URL (also saved as `apiRoute` in `infra.jsonc`).

If you later change `.prod.vars`:

```bash
npx teenybase 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.

Use this guide together with:

- [Choose Your Auth Flow](https://teenybase.com/docs/reference/auth-flows)
- [Connect Your Frontend](https://teenybase.com/docs/guides/frontend)
- [Config Reference](https://teenybase.com/docs/config)
- [Troubleshooting](https://teenybase.com/docs/reference/troubleshooting)
