> For the complete documentation index, see [llms.txt](https://docs.serverlessapigateway.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.serverlessapigateway.com/configuration/supabase-otp.md).

# Supabase OTP

This guide explains how to configure Supabase OTP (one-time password) authentication with the [Serverless API Gateway](/getting-started/introduction.md). It covers email OTP, phone OTP, `signInWithOtp`, magic link vs OTP behavior, environment variables, email templates, and troubleshooting.

If Supabase is sending magic links instead of 6-digit OTP codes, the short answer is:

* Make sure your Supabase email template renders `{{ .Token }}` instead of `{{ .ConfirmationURL }}`.
* Do not pass `emailRedirectTo` when you want numeric OTP delivery.
* Re-test the flow through the gateway's `supabase_passwordless_auth` and `supabase_passwordless_verify` endpoints.

If you are new to Serverless API Gateway, see the [Introduction](/getting-started/introduction.md) and the [Supabase Passwordless Quickstart](/reader-guides/quickstart-first-proxy/quickstart-supabase-passwordless.md) for a step-by-step walkthrough.

## Supabase signInWithOtp Overview

Supabase provides the `signInWithOtp` method for passwordless authentication. It supports two channels:

* **Email OTP** -- sends a 6-digit numeric code to the user's email address.
* **Phone OTP** -- sends a 6-digit code via SMS using a configured SMS provider (Twilio, MessageBird, Vonage, etc.).

When properly configured, the Serverless API Gateway exposes two endpoints that wrap `signInWithOtp` and its verification counterpart:

| Endpoint                       | Integration Type               | Purpose                              |
| ------------------------------ | ------------------------------ | ------------------------------------ |
| `POST /api/v1/supabase/auth`   | `supabase_passwordless_auth`   | Send OTP to email or phone           |
| `POST /api/v1/supabase/verify` | `supabase_passwordless_verify` | Verify the OTP and return JWT tokens |

For authorizer configuration details, see [Authorizer](/configuration/authorizer.md). For CORS settings when calling these endpoints from a browser, see [CORS](/configuration/cors.md).

## Quick Fix Checklist

Use this checklist before reading the full guide:

| Check                              | Correct value for OTP code flow | Why it matters                                         |
| ---------------------------------- | ------------------------------- | ------------------------------------------------------ |
| Email template variable            | `{{ .Token }}`                  | Sends a numeric OTP code instead of a link             |
| Redirect option in `signInWithOtp` | Omit `emailRedirectTo`          | Redirect URLs usually push the flow toward magic links |
| Gateway send endpoint              | `POST /api/v1/supabase/auth`    | Triggers the OTP send flow through the Worker          |
| Gateway verify endpoint            | `POST /api/v1/supabase/verify`  | Expects a numeric token, not a clicked link            |
| Supabase project settings          | Email OTP enabled               | Required for email code delivery                       |

## Magic Link vs OTP

A common point of confusion is the difference between magic links and OTP codes in Supabase:

| Behavior                               | Magic Link                         | OTP Code                                                                 |
| -------------------------------------- | ---------------------------------- | ------------------------------------------------------------------------ |
| **Delivery**                           | Email only                         | Email or SMS                                                             |
| **User action**                        | Click a link in the email          | Enter a 6-digit code in your app                                         |
| **Template variable**                  | `{{ .ConfirmationURL }}`           | `{{ .Token }}`                                                           |
| **Requires redirect URL**              | Yes                                | No                                                                       |
| **Use case**                           | Web apps that can handle redirects | Mobile apps, SPAs, or any app where you want an in-app verification flow |
| **Works with gateway verify endpoint** | Not directly                       | Yes                                                                      |

By default, Supabase may send a magic link even when you call `signInWithOtp`. The sections below explain how to force OTP code delivery instead.

## What `signInWithOtp` Creates

Supabase `signInWithOtp` starts a passwordless sign-in flow. For email, the user may receive either a magic link or a numeric OTP depending on the project settings, redirect options, and email template. For phone, the user receives an SMS OTP when SMS auth is configured.

In a typical Serverless API Gateway flow:

1. The client calls the gateway's Supabase auth endpoint with an email or phone number.
2. The gateway calls Supabase to request the OTP.
3. The user submits the OTP to the gateway's verify endpoint.
4. Supabase verifies the OTP and returns tokens.
5. Protected gateway routes validate the resulting Supabase JWT.

The important distinction is that the gateway does not replace Supabase Auth. It brokers the send and verify calls, then validates the resulting JWT on protected routes.

## Issue: Receiving Magic Links Instead of OTP Codes

When you request an email OTP, Supabase sends a magic link instead of a 6-digit OTP code. This happens because the default configuration prioritizes magic links over OTP codes.

## Why This Happens in Gateway Flows

Serverless API Gateway expects the client to:

1. Call the auth endpoint to send a code.
2. Receive a numeric OTP by email or SMS.
3. POST that token to the verify endpoint.

If Supabase sends a magic link instead, the user never receives the numeric code that `supabase_passwordless_verify` expects. The result usually looks like a broken verify step or an `invalid token` response.

## Solution: Configure Supabase Project Settings

### Step 1: Access Supabase Dashboard

1. Go to <https://supabase.com/dashboard>
2. Select your project.
3. Navigate to **Authentication** then **Settings**

### Step 2: Configure Email Auth Settings

In the **Auth Settings** section:

1. **Find "Email OTP" Settings**:
   * Look for **"Email OTP"** configuration
   * Enable **"Email OTP"** if it's disabled
2. **Disable Magic Links** (if needed):
   * Look for **"Magic Link"** settings
   * Consider disabling magic links to force OTP usage
3. **Email Template Settings**:
   * Go to **Authentication** then **Email Templates**
   * Select **"Magic Link"** template
   * Change the template type or configure it for OTP

### Step 3: Use Explicit OTP Configuration in signInWithOtp

```javascript
// In your API call, specify the type explicitly
const { data, error } = await supabase.auth.signInWithOtp({
    email: 'user@example.com',
    options: {
        emailRedirectTo: undefined, // Don't set redirect URL for magic links
        shouldCreateUser: true
    }
});
```

When `emailRedirectTo` is omitted or set to `undefined`, Supabase treats the request as an OTP request rather than a magic link request. This is the most reliable way to ensure OTP code delivery.

`shouldCreateUser` controls whether Supabase should create a new user when the email or phone does not already exist. Use `true` for sign-up or mixed sign-in/sign-up flows. Use `false` when only existing users should be allowed to request an OTP.

## signInWithOtp Options That Matter

| Option             | Recommended value for OTP code flow              | Effect                                            |
| ------------------ | ------------------------------------------------ | ------------------------------------------------- |
| `email`            | User email address                               | Sends a code by email                             |
| `phone`            | User phone number                                | Sends a code by SMS                               |
| `emailRedirectTo`  | Omit it                                          | Helps avoid magic-link behavior                   |
| `shouldCreateUser` | `true` or `false`, depending on your auth policy | Controls whether unknown users can start the flow |

Use `emailRedirectTo` only when you explicitly want a magic link flow. For numeric OTP verification behind the gateway, leave it out.

## Environment Variables

The Serverless API Gateway requires the following environment variables for Supabase OTP integration:

| Variable                    | Storage Method          | Description                                                                                                                |
| --------------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `SUPABASE_URL`              | `wrangler.toml` env var | Your Supabase project URL, e.g. `https://YOUR_PROJECT_ID.supabase.co`                                                      |
| `SUPABASE_JWT_SECRET`       | `wrangler secret put`   | JWT secret from your Supabase project settings (used by the [authorizer](/configuration/authorizer.md) to validate tokens) |
| `SUPABASE_SERVICE_ROLE_KEY` | `wrangler secret put`   | Service role key from your Supabase project settings (used server-side to call Supabase Auth API)                          |

If OTP sending fails at the gateway layer, verify these values first before debugging templates or client code.

Set environment variables in `wrangler.toml`:

```toml
[vars]
SUPABASE_URL = "https://YOUR_PROJECT_ID.supabase.co"
```

Set secrets using Wrangler:

```bash
wrangler secret put SUPABASE_JWT_SECRET
wrangler secret put SUPABASE_SERVICE_ROLE_KEY
```

## API Gateway Configuration

Add the Supabase authorizer and passwordless auth paths to your `api-config.json`:

```json
{
  "authorizer": {
    "type": "supabase",
    "jwt_secret": "$env.SUPABASE_JWT_SECRET",
    "issuer": "https://YOUR_PROJECT_ID.supabase.co/auth/v1",
    "audience": "authenticated"
  },
  "paths": [
    {
      "method": "POST",
      "path": "/api/v1/supabase/auth",
      "integration": { "type": "supabase_passwordless_auth" }
    },
    {
      "method": "POST",
      "path": "/api/v1/supabase/verify",
      "integration": { "type": "supabase_passwordless_verify" }
    },
    {
      "method": "GET",
      "path": "/api/v1/protected",
      "response": { "status": "protected endpoint" },
      "auth": true
    }
  ]
}
```

For a full configuration example including CORS, see the [Authentication Guide](/configuration/authentication.md).

## Email OTP Template Configuration

### Configure Email OTP Template

1. Go to **Authentication** then **Email Templates** in the Supabase Dashboard
2. Select **"Magic Link"** or find **"OTP"** template
3. Ensure the template contains `{{ .Token }}` instead of `{{ .ConfirmationURL }}`

Example OTP email template:

```html
<h2>Your verification code</h2>
<p>Enter this code to verify your email:</p>
<h1>{{ .Token }}</h1>
<p>This code expires in 5 minutes.</p>
```

If your template uses `{{ .ConfirmationURL }}`, the user will receive a clickable magic link. If it uses `{{ .Token }}`, the user will receive a 6-digit numeric OTP code.

## Flow Summary

| Step | Client action                                         | Gateway behavior                          | Supabase behavior                  |
| ---- | ----------------------------------------------------- | ----------------------------------------- | ---------------------------------- |
| 1    | POST email or phone to `/api/v1/supabase/auth`        | Calls `supabase_passwordless_auth`        | Sends OTP or magic link            |
| 2    | User reads email or SMS                               | No gateway step                           | Delivers the code or link          |
| 3    | POST email/phone + token to `/api/v1/supabase/verify` | Calls `supabase_passwordless_verify`      | Verifies token and returns session |
| 4    | Client uses `access_token`                            | Gateway validates JWT on protected routes | Issues tokens                      |

## Supabase Email OTP: Sending and Verifying

### Send Email OTP

```bash
curl -X POST "https://your-gateway.example.com/api/v1/supabase/auth" \
  -H "Content-Type: application/json" \
  -d '{"email": "user@example.com"}'
```

A successful response indicates the OTP was sent. The user should receive a 6-digit code in their inbox.

If the user receives a link instead of a code, check the email template first. The template must render `{{ .Token }}` for an OTP-code experience.

### Verify Email OTP

```bash
curl -X POST "https://your-gateway.example.com/api/v1/supabase/verify" \
  -H "Content-Type: application/json" \
  -d '{"email": "user@example.com", "token": "123456"}'
```

On success, the response includes an `access_token` (JWT) and a `refresh_token`. Use the access token in the `Authorization: Bearer <token>` header when calling protected endpoints.

## Supabase Phone OTP

Phone OTP sends a 6-digit code via SMS instead of email. This is useful for mobile apps or when email deliverability is a concern.

### Prerequisites for Phone OTP

1. Go to **Authentication** then **Settings** in the Supabase Dashboard
2. Find the **"Phone Auth"** section
3. Enable phone authentication
4. Configure your SMS provider (Twilio, MessageBird, Vonage, etc.)
5. Enter the required credentials for your SMS provider

### Send Phone OTP

```bash
curl -X POST "https://your-gateway.example.com/api/v1/supabase/auth" \
  -H "Content-Type: application/json" \
  -d '{"phone": "+1234567890"}'
```

### Verify Phone OTP

```bash
curl -X POST "https://your-gateway.example.com/api/v1/supabase/verify" \
  -H "Content-Type: application/json" \
  -d '{"phone": "+1234567890", "token": "123456"}'
```

The verification response is identical in structure to email OTP verification: you receive a JWT access token and refresh token.

## Troubleshooting

### Still Receiving Magic Links Instead of OTP Codes

Check these in order:

1. **Email template**: Verify the template uses `{{ .Token }}` and not `{{ .ConfirmationURL }}`.
2. **Project settings**: Ensure Email OTP is enabled in Supabase Auth settings.
3. **`emailRedirectTo`**: Remove it from `signInWithOtp`. If it is set, Supabase can switch to a link-based flow.
4. **Gateway flow**: Make sure the client is using `/api/v1/supabase/auth` and `/api/v1/supabase/verify`, not mixing direct Supabase calls with gateway endpoints.
5. **Retest with a fresh email**: This helps rule out template caching or rate-limit noise.

### OTP Code Not Arriving

Check these in order:

1. Look in spam or junk folders.
2. Verify the email address or phone number is correct.
3. Check Supabase Dashboard logs for delivery errors.
4. Confirm your project has not exceeded email or SMS sending limits.
5. For phone OTP, verify the SMS provider credentials and provider logs.

### Token Verification Fails

Check these in order:

1. Confirm the OTP code has not expired.
2. Confirm the client is POSTing to `/api/v1/supabase/verify`.
3. Check that the token is a numeric OTP code, not a clicked magic-link flow.
4. Verify `SUPABASE_JWT_SECRET` and `SUPABASE_SERVICE_ROLE_KEY`.
5. Verify the `issuer` field in the authorizer matches your Supabase project URL.
6. Re-test after sending a brand new code so you are not verifying an old token.

### Code-Level Fix (If Dashboard Configuration Does Not Work)

If the dashboard configuration does not resolve the issue, try using the admin client with explicit OTP type:

```javascript
// Try using admin client with explicit OTP type
const supabase = createClient(
    process.env.SUPABASE_URL, 
    process.env.SUPABASE_SERVICE_ROLE_KEY  // Use service role key
);

const { data, error } = await supabase.auth.admin.generateLink({
    type: 'signup',  // or 'signin'
    email: email,
    options: {
        redirectTo: undefined  // No redirect for OTP
    }
});
```

## Expected Behavior After Configuration

After proper configuration:

* **Email OTP**: You receive a 6-digit code like `123456` in your email.
* **Phone OTP**: You receive a 6-digit code via SMS.
* **Send response**: The auth endpoint confirms that Supabase accepted the OTP request.
* **Verification response**: The verify endpoint returns `access_token` and `refresh_token`.

The key is ensuring your Supabase project is configured to prioritize OTP codes over magic links in the authentication flow.

## Related Pages

* [Authentication Guide](/configuration/authentication.md) -- full authentication setup for Serverless API Gateway
* [Authorizer Configuration](/configuration/authorizer.md) -- JWT and provider-based authorization
* [CORS Configuration](/configuration/cors.md) -- configure cross-origin requests for browser-based OTP flows
* [Auth0 Integration](/configuration/auth0.md) -- alternative authentication provider
* [Supabase Passwordless Quickstart](/reader-guides/quickstart-first-proxy/quickstart-supabase-passwordless.md) -- step-by-step quickstart guide
* [Introduction](/getting-started/introduction.md) -- overview of Serverless API Gateway


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.serverlessapigateway.com/configuration/supabase-otp.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
