# 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, magic link behavior, the `signInWithOtp` method, environment variables, email templates, and troubleshooting.

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).

## Magic Link vs OTP

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

|                           | 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 |

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

## 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.

## 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.

## 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)                          |

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.

## 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.

### 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

1. **Check Email Templates**: Verify the template uses `{{ .Token }}` not `{{ .ConfirmationURL }}`
2. **Project Settings**: Ensure OTP is enabled in the Supabase Auth settings
3. **emailRedirectTo**: Make sure you are not passing a redirect URL in the `signInWithOtp` options. If `emailRedirectTo` is set, Supabase sends a magic link.
4. **Cache**: Clear browser cache and try again
5. **Different Email**: Try with a different email address to rule out rate limiting

### OTP Code Not Arriving

1. Check your spam/junk folder
2. Verify the email address is correct
3. Check the Supabase Dashboard **Logs** section for delivery errors
4. Confirm your Supabase project has not exceeded its email sending quota
5. For phone OTP, verify your SMS provider credentials and check the provider's delivery logs

### Token Verification Fails

1. Ensure the OTP code has not expired (default: 5 minutes)
2. Confirm you are sending the token to the correct verify endpoint
3. Check that `SUPABASE_JWT_SECRET` and `SUPABASE_SERVICE_ROLE_KEY` are set correctly as Wrangler secrets
4. Verify the `issuer` field in your authorizer config matches your Supabase project URL

### 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
* **Response**: The API returns a success message confirming the OTP was sent
* **Verification**: Use the 6-digit code to verify and receive JWT tokens (access token + 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: 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.
