# Email and password sign-in

> Sign users up and in with an email and password on SupaHost, confirm their address, set the minimum password length, and build a working password reset flow.

Source: https://supahost.app/docs/auth/passwords · Updated 2026-11-11

Email and password is on by default for every SupaHost project. A user signs up, confirms their address from an email Auth sends, and then signs in to get a session. Passwords are hashed with bcrypt by Auth and never stored in plain text.

## Settings that apply

*Dashboard → your project → **Auth** tab → **Sign-in & security***:

| Setting | Effect |
|---|---|
| Email sign-in | Turns email and password, magic links and email OTP on or off together. On by default |
| Disable new sign-ups | Existing users can still sign in, and you can still invite people. `signUp` returns an error |
| Skip email confirmation | Users get a session straight from `signUp` without confirming. Handy in development, risky in production |
| Minimum password length | 6 to 72 characters. Default 6 |

Click **Save sign-in settings**. The change reaches the project within a minute. The minimum length is the only password rule you can set on SupaHost; if you want more (a mix of character types, a blocklist), check it in your app before calling `signUp`.

Also set the **Site URL** on the **Auth** card, because confirmation and reset links fall back to it. See [Site URL and redirect URLs](/docs/auth/redirect-urls).

## Sign up

```ts
const { data, error } = await supabase.auth.signUp({
  email: 'alex@example.com',
  password: 'plum-orchard-42-lantern',
  options: {
    emailRedirectTo: 'https://invoices.example.com/auth/callback',
    data: { company: 'Alex Design Ltd' },
  },
})
```

With confirmation on (the default), `data.user` is set but `data.session` is `null`. Auth sends the **Confirm sign-up** email. It contains a button with a link and, below it, a one-time code. Either works:

- The **link** goes to `/auth/v1/verify` on your project, which confirms the address and redirects to `emailRedirectTo` (or the Site URL) with a session.
- The **code** lets the user confirm without leaving your app, which helps on mobile:

```ts
const { data, error } = await supabase.auth.verifyOtp({
  email: 'alex@example.com',
  token: codeFromInput,
  type: 'email',
})
```

If `emailRedirectTo` isn't in your Redirect URLs list, Auth ignores it and uses the Site URL instead.

### When the email is already registered

With confirmation on, signing up with an address that already has an account doesn't return an error, so attackers can't use the form to find out who has an account. For an already confirmed account, the response has a user object with an empty `identities` array and no session. If you want to show "you already have an account", check `data.user?.identities?.length === 0`.

## Sign in

```ts
const { data, error } = await supabase.auth.signInWithPassword({
  email: 'alex@example.com',
  password: passwordFromInput,
})

if (error?.code === 'invalid_credentials') {
  // wrong email or password; don't say which
}
if (error?.code === 'email_not_confirmed') {
  // offer to resend the confirmation email
}
```

Resend a confirmation email:

```ts
await supabase.auth.resend({ type: 'signup', email: 'alex@example.com' })
```

Every email counts toward the project's email limit (30 per rate-limit window, fixed on SupaHost), so don't resend automatically in a loop. See [Rate limits](/docs/auth/rate-limits).

## Password reset

A reset has two steps: send the email, then let the user choose a new password once they're signed in through the link.

**1. Send the reset email** from a "Forgot password" form:

```ts
await supabase.auth.resetPasswordForEmail('alex@example.com', {
  redirectTo: 'https://invoices.example.com/account/new-password',
})
```

Auth sends the **Reset password** template. Add `https://invoices.example.com/account/new-password` (or a wildcard that covers it) to Redirect URLs first.

**2. Set the new password.** When the user follows the link, they arrive on that page signed in. In a browser-only app, the client library fires a `PASSWORD_RECOVERY` event you can listen for. In both browser and server apps, the page can simply call `updateUser`:

```ts
const { error } = await supabase.auth.updateUser({ password: newPassword })
```

If you use server-side auth with the PKCE flow, the link arrives with a `?code=` parameter. Exchange it in your callback route first (see [Server-side auth](/docs/auth/server-side)), then redirect to the new-password page. The user can also type the code from the email instead:

```ts
await supabase.auth.verifyOtp({ email, token: codeFromEmail, type: 'recovery' })
await supabase.auth.updateUser({ password: newPassword })
```

## Changing a password while signed in

The same `updateUser` call works for a signed-in user on a settings page. Ask for the current password in your UI and check it by signing in again before you call it, so someone who walks up to an unlocked laptop can't change it.

## Signing out

```ts
await supabase.auth.signOut()                    // all of this user's sessions (default)
await supabase.auth.signOut({ scope: 'local' })  // only this device
```

## Common problems

- Emails don't arrive: check spam, then the rate limit, then the Site URL. See [Not receiving Auth emails](/kb/not-receiving-auth-emails).
- The link says it's expired: links are single use, and some email scanners open them before the user does. See [OTP or magic link expired](/kb/otp-expired).
- The link lands on the wrong site: see [Redirected to the wrong URL](/kb/redirect-to-wrong-url).

For every option on these methods, see the [supabase-js reference for signUp](https://supabase.com/docs/reference/javascript/auth-signup).
