diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts
index 4cea890356d54..0ca56c3328495 100644
--- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts
+++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts
@@ -1,8 +1,9 @@
-import type { ComponentProps } from 'react'
// End of third-party imports
import { isFeatureEnabled } from 'common/enabled-features'
+import type { ComponentProps } from 'react'
import type { IconPanel } from 'ui-patterns/IconPanel'
+
import type { GlobalMenuItems, NavMenuConstant, NavMenuSection } from '../Navigation.types'
const {
@@ -2849,10 +2850,19 @@ export const self_hosting: NavMenuConstant = {
name: 'How-to Guides',
items: [
{ name: 'Self-Hosted Functions', url: '/guides/self-hosting/self-hosted-functions' },
- { name: 'Restore from Platform', url: '/guides/self-hosting/restore-from-platform' },
+ {
+ name: 'Add Reverse Proxy with HTTPS',
+ url: '/guides/self-hosting/self-hosted-proxy-https',
+ },
+ {
+ name: 'Restore Project from Platform',
+ url: '/guides/self-hosting/restore-from-platform',
+ },
{ name: 'Configure S3 Storage', url: '/guides/self-hosting/self-hosted-s3' },
{ name: 'Copy Storage from Platform', url: '/guides/self-hosting/copy-from-platform-s3' },
- { name: 'Enabling MCP server', url: '/guides/self-hosting/enable-mcp' },
+ { name: 'Configure Social Login (OAuth)', url: '/guides/self-hosting/self-hosted-oauth' },
+ { name: 'Configure Phone Login & MFA', url: '/guides/self-hosting/self-hosted-phone-mfa' },
+ { name: 'Enable MCP server', url: '/guides/self-hosting/enable-mcp' },
],
},
{
diff --git a/apps/docs/content/guides/self-hosting/docker.mdx b/apps/docs/content/guides/self-hosting/docker.mdx
index 74146dcdc8db2..a196650d77b21 100644
--- a/apps/docs/content/guides/self-hosting/docker.mdx
+++ b/apps/docs/content/guides/self-hosting/docker.mdx
@@ -428,6 +428,20 @@ By default all files are stored locally on the server. You can connect Storage t
See the [Configure S3 Storage](/docs/guides/self-hosting/self-hosted-s3) guide for detailed setup instructions.
+#### Configuring HTTPS
+
+By default, Supabase is accessible over HTTP. For production deployments, especially when using OAuth providers, you need HTTPS with a valid TLS certificate. The recommended approach is to place a reverse proxy (such as Caddy or Nginx) in front of Kong.
+
+See the [Configure HTTPS](/docs/guides/self-hosting/self-hosted-proxy-https) guide for setup instructions.
+
+#### Configuring social login (OAuth) providers
+
+See the [Configure Social Login (OAuth) Providers](/docs/guides/self-hosting/self-hosted-oauth) guide for setup instructions.
+
+#### Configuring phone login, SMS, and MFA
+
+See the [Configure Phone Login & MFA](/docs/guides/self-hosting/self-hosted-phone-mfa) guide for SMS provider setup, OTP settings, and multi-factor authentication configuration.
+
#### Configuring Supabase AI Assistant
Configuring the Supabase AI Assistant is optional. By adding **your own** `OPENAI_API_KEY` to `.env` you can enable AI services, which help with writing SQL queries, statements, and policies.
diff --git a/apps/docs/content/guides/self-hosting/self-hosted-oauth.mdx b/apps/docs/content/guides/self-hosting/self-hosted-oauth.mdx
new file mode 100644
index 0000000000000..702cdd8756547
--- /dev/null
+++ b/apps/docs/content/guides/self-hosting/self-hosted-oauth.mdx
@@ -0,0 +1,458 @@
+---
+title: 'Configure Social Login (OAuth) Providers'
+description: 'Set up social login (OAuth/OIDC) providers for self-hosted Supabase with Docker.'
+subtitle: 'Set up social login (OAuth/OIDC) providers for self-hosted Supabase with Docker.'
+---
+
+This guide covers the **server-side configuration** required to enable social login providers on a self-hosted Supabase instance running with Docker Compose. This applies to all OAuth and OIDC-based providers, including third-party identity providers like Keycloak.
+
+## Before you begin
+
+You need:
+
+- A working self-hosted Supabase installation. See [Self-Hosting with Docker](/docs/guides/self-hosting/docker).
+- `API_EXTERNAL_URL` set to the publicly reachable URL of your Supabase instance (e.g., `https://`).
+
+
+
+HTTPS is strongly recommended in production. Most OAuth providers reject `http://` callback URLs (except `localhost`).
+
+
+
+Your **OAuth callback URL** is built from `API_EXTERNAL_URL`. For example, if `API_EXTERNAL_URL` is `https://`, the callback URL will become:
+
+```
+https:///auth/v1/callback
+```
+
+You will have to register this URL with each OAuth provider.
+
+## OAuth request flow
+
+When a user signs in with an OAuth provider, the following flow occurs:
+
+1. Your app calls `supabase.auth.signInWithOAuth()` and the browser redirects to the Auth service
+2. API gateway (Kong) routes the request to the Auth container (`/auth/v1/authorize`)
+3. Auth redirects the user to the OAuth provider (e.g., Google) for consent
+4. The provider redirects back to `https:///auth/v1/callback`
+5. Auth exchanges the authorization code for tokens and redirects the user to your `SITE_URL` or an allowed redirect URL
+
+## Auth environment variables
+
+The Auth service (GoTrue) uses the prefix `GOTRUE_EXTERNAL_` followed by a provider name for all OAuth configuration. For example, when using Google:
+
+- `GOTRUE_EXTERNAL_GOOGLE_ENABLED`
+- `GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID`
+- `GOTRUE_EXTERNAL_GOOGLE_SECRET`
+- `GOTRUE_EXTERNAL_GOOGLE_REDIRECT_URI`
+
+## Step-by-step configuration
+
+The default `.env.example` and `docker-compose.yml` include commented-out placeholders for Google, GitHub, and Azure.
+
+### Step 1: Register your app with the provider
+
+1. Go to the OAuth provider's developer console and create an application.
+2. Set the **authorized redirect URL**, e.g., `https:///auth/v1/callback`
+3. Copy the **client ID** and **client secret** into your `.env` file.
+
+### Step 2: Configure variables in the `.env` file
+
+Uncomment the lines for your provider in `.env` and add your client ID and secret, e.g., for Google:
+
+```
+GOOGLE_ENABLED=true
+GOOGLE_CLIENT_ID=your-client-id
+GOOGLE_SECRET=your-client-secret
+```
+
+### Step 3: Enable the matching lines in `docker-compose.yml`
+
+Uncomment the corresponding `GOTRUE_EXTERNAL_` lines in the `auth` service's `environment`:
+
+```
+GOTRUE_EXTERNAL_GOOGLE_ENABLED: ${GOOGLE_ENABLED}
+GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID}
+GOTRUE_EXTERNAL_GOOGLE_SECRET: ${GOOGLE_SECRET}
+GOTRUE_EXTERNAL_GOOGLE_REDIRECT_URI: ${API_EXTERNAL_URL}/auth/v1/callback
+```
+
+
+
+For providers **not** pre-configured in the files (see the [full provider list](#other-supported-providers) below), add the lines manually following the same pattern: variables in `.env`, passthrough with `GOTRUE_EXTERNAL_PROVIDER_` in `docker-compose.yml`.
+
+
+
+### Step 4: Restart the auth service
+
+```sh
+docker compose up -d --force-recreate --no-deps auth
+```
+
+### Step 5: Verify the configuration
+
+Check that the provider is enabled:
+
+```sh
+curl https:///auth/v1/settings
+```
+
+The response should include your provider under `external`:
+
+```
+{
+ "external": {
+ "google": true
+ }
+}
+```
+
+## Provider-specific setup
+
+
+
+
+
+**Google Cloud Console setup:**
+
+1. Go to [Google Cloud Console](https://console.cloud.google.com/)
+2. Create or select a project
+3. Select **Solutions** > **All products** in the navigation menu on the left
+4. Go to **APIs & services** > **OAuth consent screen** and click **Get started**
+5. Follow the configuration steps and add an **External** app
+6. Go to **APIs & Services** > **Credentials**
+7. Click **Create Credentials** > **OAuth client ID**
+8. Set application type to **Web application**
+9. Under **Authorized redirect URIs**, add: `https:///auth/v1/callback`
+10. Click **Create** and copy the client ID and client secret
+
+**`.env`:**
+
+```
+GOOGLE_ENABLED=true
+GOOGLE_CLIENT_ID=your-google-client-id.apps.googleusercontent.com
+GOOGLE_SECRET=your-google-client-secret
+```
+
+**`docker-compose.yml`:**
+
+```
+GOTRUE_EXTERNAL_GOOGLE_ENABLED: ${GOOGLE_ENABLED}
+GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID}
+GOTRUE_EXTERNAL_GOOGLE_SECRET: ${GOOGLE_SECRET}
+GOTRUE_EXTERNAL_GOOGLE_REDIRECT_URI: ${API_EXTERNAL_URL}/auth/v1/callback
+```
+
+
+
+
+
+**GitHub setup:**
+
+1. Go to [GitHub Developer Settings](https://github.com/settings/developers)
+2. Click **New OAuth app**
+3. Fill in **Homepage URL**, e.g., `https://`
+4. Set **Authorization callback URL** to: `https:///auth/v1/callback`
+5. Click **Register application**
+6. Copy the client ID, generate and copy a client secret
+
+**`.env`:**
+
+```
+GITHUB_ENABLED=true
+GITHUB_CLIENT_ID=your-github-client-id
+GITHUB_SECRET=your-github-client-secret
+```
+
+**`docker-compose.yml`:**
+
+```
+GOTRUE_EXTERNAL_GITHUB_ENABLED: ${GITHUB_ENABLED}
+GOTRUE_EXTERNAL_GITHUB_CLIENT_ID: ${GITHUB_CLIENT_ID}
+GOTRUE_EXTERNAL_GITHUB_SECRET: ${GITHUB_SECRET}
+GOTRUE_EXTERNAL_GITHUB_REDIRECT_URI: ${API_EXTERNAL_URL}/auth/v1/callback
+```
+
+
+
+
+
+**Azure Portal setup:**
+
+1. Go to [Azure Portal](https://portal.azure.com/)
+2. Go to **All services** > **Identity** > **App registrations** via the navigation menu on the left
+3. Click **New registration**
+4. Add application **Name**
+5. Under **Redirect URI**, select **Web** and enter: `https:///auth/v1/callback`
+6. Click **Register**
+7. Copy the **Application (client) ID**
+8. Click on **Client credentials** > **Add a certificate or secret**
+9. Click on **New client secret** and add a client secret
+10. Copy the secret value (not "secret ID")
+
+**`.env`:**
+
+```
+AZURE_ENABLED=true
+AZURE_CLIENT_ID=your-azure-application-client-id
+AZURE_SECRET=your-azure-client-secret
+## Optional: restrict to a specific tenant (defaults to 'common')
+# AZURE_URL=https://login.microsoftonline.com/your-tenant-id
+```
+
+**`docker-compose.yml`:**
+
+```
+GOTRUE_EXTERNAL_AZURE_ENABLED: ${AZURE_ENABLED}
+GOTRUE_EXTERNAL_AZURE_CLIENT_ID: ${AZURE_CLIENT_ID}
+GOTRUE_EXTERNAL_AZURE_SECRET: ${AZURE_SECRET}
+GOTRUE_EXTERNAL_AZURE_REDIRECT_URI: ${API_EXTERNAL_URL}/auth/v1/callback
+## Optional: uncomment for tenant-specific Azure login
+# GOTRUE_EXTERNAL_AZURE_URL: ${AZURE_URL}
+```
+
+
+
+
+
+**Apple Developer setup:**
+
+1. Refer to [Apple Developer documentation](https://developer.apple.com/documentation/signinwithapple/configuring-your-environment-for-sign-in-with-apple) to learn how to enable App ID and create a Services ID
+2. Create a private key for sign in with Apple
+3. Generate a client secret JWT from your private key. See [Apple Developer documentation](https://developer.apple.com/documentation/accountorganizationaldatasharing/creating-a-client-secret) for details.
+
+**`.env`:**
+
+```
+APPLE_ENABLED=true
+APPLE_CLIENT_ID=com.example.your-services-id
+APPLE_SECRET=your-generated-jwt-client-secret
+```
+
+**`docker-compose.yml`:**
+
+```
+GOTRUE_EXTERNAL_APPLE_ENABLED: ${APPLE_ENABLED}
+GOTRUE_EXTERNAL_APPLE_CLIENT_ID: ${APPLE_CLIENT_ID}
+GOTRUE_EXTERNAL_APPLE_SECRET: ${APPLE_SECRET}
+GOTRUE_EXTERNAL_APPLE_REDIRECT_URI: ${API_EXTERNAL_URL}/auth/v1/callback
+```
+
+
+
+Apple uses `response_mode=form_post` for its OAuth flow. The Auth service handles this automatically - no additional configuration is needed.
+
+
+
+
+
+
+
+**Keycloak setup:**
+
+1. Open your Keycloak admin console
+2. Select (or create) the realm you want to use
+3. Go to **Clients** > **Create client**
+4. Set **Client type** to **OpenID Connect**
+5. Set **Client ID** (e.g., `supabase`)
+6. On the next screen, enable **Client authentication**
+7. Under **Valid redirect URIs**, add: `https:///auth/v1/callback`
+8. Save, then go to the **Credentials** tab and copy the **Client secret**
+
+**`.env` variables:**
+
+```
+KEYCLOAK_ENABLED=true
+KEYCLOAK_CLIENT_ID=supabase
+KEYCLOAK_SECRET=your-keycloak-client-secret
+## Required: your Keycloak realm URL
+KEYCLOAK_URL=https://keycloak.example.com/realms/myrealm
+```
+
+**`docker-compose.yml` passthrough:**
+
+```
+GOTRUE_EXTERNAL_KEYCLOAK_ENABLED: ${KEYCLOAK_ENABLED}
+GOTRUE_EXTERNAL_KEYCLOAK_CLIENT_ID: ${KEYCLOAK_CLIENT_ID}
+GOTRUE_EXTERNAL_KEYCLOAK_SECRET: ${KEYCLOAK_SECRET}
+GOTRUE_EXTERNAL_KEYCLOAK_REDIRECT_URI: ${API_EXTERNAL_URL}/auth/v1/callback
+GOTRUE_EXTERNAL_KEYCLOAK_URL: ${KEYCLOAK_URL}
+```
+
+
+
+`KEYCLOAK_URL` is **required**. It must be the full realm URL (e.g., `https://keycloak.example.com/realms/myrealm`). The Auth service uses this to discover the OIDC endpoints (`.well-known/openid-configuration`). Without it, Keycloak login will not work.
+
+
+
+
+
+
+
+## Other supported providers
+
+Supabase Auth supports the following OAuth providers:
+
+| Provider | Env prefix | Additional variables | Docs |
+| ----------------- | ---------------- | ------------------------------- | --------------------------------------------------------------------- |
+| Apple | `APPLE_` | - | [Login with Apple](/docs/guides/auth/social-login/auth-apple) |
+| Azure (Microsoft) | `AZURE_` | `URL` (tenant URL) | [Login with Azure](/docs/guides/auth/social-login/auth-azure) |
+| Bitbucket | `BITBUCKET_` | - | [Login with Bitbucket](/docs/guides/auth/social-login/auth-bitbucket) |
+| Discord | `DISCORD_` | - | [Login with Discord](/docs/guides/auth/social-login/auth-discord) |
+| Facebook | `FACEBOOK_` | - | [Login with Facebook](/docs/guides/auth/social-login/auth-facebook) |
+| Figma | `FIGMA_` | - | [Login with Figma](/docs/guides/auth/social-login/auth-figma) |
+| GitHub | `GITHUB_` | `URL` (for GitHub Enterprise) | [Login with GitHub](/docs/guides/auth/social-login/auth-github) |
+| GitLab | `GITLAB_` | `URL` (for self-hosted GitLab) | [Login with GitLab](/docs/guides/auth/social-login/auth-gitlab) |
+| Google | `GOOGLE_` | - | [Login with Google](/docs/guides/auth/social-login/auth-google) |
+| Kakao | `KAKAO_` | - | [Login with Kakao](/docs/guides/auth/social-login/auth-kakao) |
+| Keycloak (OIDC) | `KEYCLOAK_` | `URL` (realm URL, **required**) | [Login with Keycloak](/docs/guides/auth/social-login/auth-keycloak) |
+| LinkedIn (OIDC) | `LINKEDIN_OIDC_` | - | [Login with LinkedIn](/docs/guides/auth/social-login/auth-linkedin) |
+| Notion | `NOTION_` | - | [Login with Notion](/docs/guides/auth/social-login/auth-notion) |
+| Slack (OIDC) | `SLACK_OIDC_` | - | [Login with Slack](/docs/guides/auth/social-login/auth-slack) |
+| Snapchat | `SNAPCHAT_` | - | - |
+| Spotify | `SPOTIFY_` | - | [Login with Spotify](/docs/guides/auth/social-login/auth-spotify) |
+| Twitch | `TWITCH_` | - | [Login with Twitch](/docs/guides/auth/social-login/auth-twitch) |
+| Twitter | `TWITTER_` | - | [Login with Twitter](/docs/guides/auth/social-login/auth-twitter) |
+| WorkOS | `WORKOS_` | - | [Login with WorkOS](/docs/guides/auth/social-login/auth-workos) |
+| Zoom | `ZOOM_` | - | [Login with Zoom](/docs/guides/auth/social-login/auth-zoom) |
+
+For each provider, you need at minimum `ENABLED`, `CLIENT_ID`, `SECRET`, and `REDIRECT_URI` in `.env` and `docker-compose.yml`.
+
+
+
+**LinkedIn (OIDC)** and **Slack (OIDC)** use multi-word env prefixes. The full Docker Compose variables are `GOTRUE_EXTERNAL_LINKEDIN_OIDC_CLIENT_ID` and `GOTRUE_EXTERNAL_SLACK_OIDC_CLIENT_ID` respectively - not `LINKEDIN_CLIENT_ID` or `SLACK_CLIENT_ID`.
+
+
+
+## Test the login flow
+
+You can test OAuth with the following minimal `index.html` served, e.g., via `python -m http.server 3000`:
+
+```html
+
+
+
+
Supabase OAuth Test
+
+
+
+
+
+
+
+```
+
+
+
+Make sure `SITE_URL` or `ADDITIONAL_REDIRECT_URLS` includes the URL where you open this test page. Otherwise the Auth service will reject the redirect after login.
+
+
+
+For detailed client-side integration, see [Social Login](/docs/guides/auth/social-login).
+
+## Troubleshooting
+
+### "Provider not enabled" or provider shows `false` in `/auth/v1/settings`
+
+- Check that `GOTRUE_EXTERNAL_*_ENABLED` is set to `true` in `docker-compose.yml`
+- Verify the `.env` variable is not empty, e.g., check with `docker compose exec auth env | grep GOOGLE`
+
+### Variables added to `.env` but provider still not working
+
+Configuration variables from `.env` are **not** automatically available inside the container unless there's a matching passthrough definition in `docker-compose.yml`. Check, e.g., for:
+
+```
+GOTRUE_EXTERNAL_GOOGLE_ENABLED: ${GOOGLE_ENABLED}
+```
+
+Run `docker compose exec auth env | grep GOTRUE_EXTERNAL` to verify the variables are reaching the container.
+
+### `SITE_URL` or redirect URL errors after login
+
+After a successful OAuth login, the Auth service redirects to `SITE_URL` or a URL from `ADDITIONAL_REDIRECT_URLS`. Ensure:
+
+- `SITE_URL` in `.env` is set to your application's URL
+- If your app uses a different redirect URL, add it to `ADDITIONAL_REDIRECT_URLS` (comma-separated)
+
+{/* supa-mdx-lint-disable-next-line Rule001HeadingCase */}
+
+### Nonce check failure on mobile (Google Sign In)
+
+When using Google Sign In on mobile with ID tokens, nonce verification may fail because mobile SDKs don't always support the nonce flow that the Auth service expects.
+
+
+
+`GOTRUE_EXTERNAL_SKIP_NONCE_CHECK` disables nonce validation on ID tokens, which weakens replay-attack protection. Treat it as a **short-lived troubleshooting workaround**, not a permanent fix:
+
+- Enable it only in the environment where you're debugging the issue.
+- Revert it as soon as the issue is resolved.
+- Prefer fixing the client-side nonce handling or switching to an OAuth flow (authorization code with PKCE) that avoids ID-token nonce issues entirely.
+
+
+
+To enable it, uncomment the following line in `docker-compose.yml`:
+
+```
+GOTRUE_EXTERNAL_SKIP_NONCE_CHECK: true
+```
+
+### Auth service fails to start
+
+Check the auth container logs:
+
+```sh
+docker compose logs auth
+```
+
+Common causes:
+
+- Missing required environment variable (e.g., `CLIENT_ID` or `SECRET` is empty)
+- Invalid `API_EXTERNAL_URL` (must be a valid URL with protocol)
+
+## Environment variable reference
+
+All OAuth-related environment variables for the `auth` service in `docker-compose.yml`:
+
+| Variable | Description | Required |
+| -------------------------------- | ------------------------------------------------------------------------ | -------- |
+| `GOTRUE_EXTERNAL_*_ENABLED` | Enable the provider (`true`/`false`) | Yes |
+| `GOTRUE_EXTERNAL_*_CLIENT_ID` | OAuth client ID from the provider | Yes |
+| `GOTRUE_EXTERNAL_*_SECRET` | OAuth client secret from the provider | Yes |
+| `GOTRUE_EXTERNAL_*_REDIRECT_URI` | Callback URL: `${API_EXTERNAL_URL}/auth/v1/callback` | Yes |
+| `GOTRUE_SITE_URL` | Default redirect URL after authentication (set via `SITE_URL` in `.env`) | Yes |
+
+## Additional resources
+
+- [Redirect URLs](/docs/guides/auth/redirect-urls)
+- [Auth server on GitHub](https://github.com/supabase/auth) (check README and `example.env`)
diff --git a/apps/docs/content/guides/self-hosting/self-hosted-phone-mfa.mdx b/apps/docs/content/guides/self-hosting/self-hosted-phone-mfa.mdx
new file mode 100644
index 0000000000000..64d2d13ba748e
--- /dev/null
+++ b/apps/docs/content/guides/self-hosting/self-hosted-phone-mfa.mdx
@@ -0,0 +1,222 @@
+---
+title: 'Configure Phone Login & MFA'
+description: 'Set up phone login SMS providers, OTP settings, and multi-factor authentication for self-hosted Supabase with Docker.'
+subtitle: 'Set up phone login SMS providers, OTP settings, and multi-factor authentication for self-hosted Supabase with Docker.'
+---
+
+This guide covers the **server-side configuration** for phone login and multi-factor authentication (MFA) on a self-hosted Supabase instance running with Docker Compose.
+
+For client-side implementation, see [Phone Login](/docs/guides/auth/phone-login) and [Multi-Factor Authentication](/docs/guides/auth/auth-mfa).
+
+## Before you begin
+
+You need:
+
+- A working self-hosted Supabase installation. See [Self-Hosting with Docker](/docs/guides/self-hosting/docker).
+- An account with an SMS provider (e.g., Twilio)
+
+Phone auth is **enabled by default** in the Docker setup (`ENABLE_PHONE_SIGNUP=true` in `.env`). However, without an SMS provider configured, the Auth service has no way to deliver OTP codes.
+
+## SMS provider configuration
+
+The default `.env.example` and `docker-compose.yml` include commented-out SMS provider placeholders. The example below uses Twilio - you'll need a Twilio account with an account SID, auth token, and message service SID. See [Twilio's documentation](https://www.twilio.com/docs/messaging) for how to obtain these credentials.
+
+To enable SMS delivery:
+
+### Step 1: Uncomment and configure the settings in `.env`
+
+```
+SMS_PROVIDER=twilio
+SMS_OTP_EXP=60
+SMS_OTP_LENGTH=6
+SMS_MAX_FREQUENCY=60s
+SMS_TEMPLATE=Your code is {{ .Code }}
+
+## Twilio credentials
+SMS_TWILIO_ACCOUNT_SID=your-account-sid
+SMS_TWILIO_AUTH_TOKEN=your-auth-token
+SMS_TWILIO_MESSAGE_SERVICE_SID=your-message-service-sid
+```
+
+### Step 2: Uncomment the matching lines in `docker-compose.yml`
+
+Uncomment the `GOTRUE_SMS_*` lines in the `auth` service's `environment` block:
+
+```
+GOTRUE_SMS_PROVIDER: ${SMS_PROVIDER}
+GOTRUE_SMS_OTP_EXP: ${SMS_OTP_EXP}
+GOTRUE_SMS_OTP_LENGTH: ${SMS_OTP_LENGTH}
+GOTRUE_SMS_MAX_FREQUENCY: ${SMS_MAX_FREQUENCY}
+GOTRUE_SMS_TEMPLATE: ${SMS_TEMPLATE}
+GOTRUE_SMS_TWILIO_ACCOUNT_SID: ${SMS_TWILIO_ACCOUNT_SID}
+GOTRUE_SMS_TWILIO_AUTH_TOKEN: ${SMS_TWILIO_AUTH_TOKEN}
+GOTRUE_SMS_TWILIO_MESSAGE_SERVICE_SID: ${SMS_TWILIO_MESSAGE_SERVICE_SID}
+```
+
+### Step 3: Restart the auth service
+
+```sh
+docker compose up -d --force-recreate --no-deps auth
+```
+
+### Step 4: Verify
+
+```sh
+docker compose exec auth env | grep GOTRUE_SMS
+```
+
+Confirm your provider and credentials appear in the output.
+
+
+
+For providers other than Twilio, add the provider-specific `GOTRUE_SMS_*` lines manually to `docker-compose.yml`.
+
+
+
+## OTP settings
+
+### Expiration
+
+
+
+The default OTP expiration is **60 seconds**. This is often too short for production use, consider increasing it.
+
+
+
+Set `SMS_OTP_EXP` in `.env` (value is in seconds):
+
+```
+# Set expiration to 5 minutes
+SMS_OTP_EXP=300
+```
+
+And ensure `GOTRUE_SMS_OTP_EXP: ${SMS_OTP_EXP}` is uncommented in `docker-compose.yml`.
+
+### Length
+
+The default OTP length is 6 digits. You can set it to any value between 6 and 10:
+
+```
+SMS_OTP_LENGTH=8
+```
+
+### Rate limiting
+
+`SMS_MAX_FREQUENCY` controls the minimum interval between SMS sends to the same phone number. The default is 60 seconds:
+
+```
+## Allow one SMS every 30 seconds
+SMS_MAX_FREQUENCY=30s
+```
+
+## Test OTPs for development
+
+To avoid sending real SMS during development, use `SMS_TEST_OTP` to map phone numbers to fixed OTP codes:
+
+```
+SMS_TEST_OTP=16505551234:123456,16505555678:654321
+```
+
+And uncomment `GOTRUE_SMS_TEST_OTP: ${SMS_TEST_OTP}` in `docker-compose.yml`.
+
+When a test phone number requests an OTP, the Auth service skips SMS delivery and accepts only the mapped code. Other phone numbers continue to use the real SMS provider.
+
+
+
+Remove test OTPs before deploying to production. You can also set an expiration with `SMS_TEST_OTP_VALID_UNTIL` (ISO 8601 datetime, e.g., `2026-12-31T23:59:59Z`) so they stop working automatically.
+
+
+
+## Multi-factor authentication (MFA)
+
+The Auth service supports three MFA factor types. Configure them by uncommenting variables in `.env` and the matching `GOTRUE_MFA_*` lines in `docker-compose.yml`.
+
+### App authenticator (TOTP)
+
+TOTP is **enabled by default** - users can enroll with apps like Google Authenticator or Authy without any additional configuration.
+
+To disable TOTP:
+
+```
+MFA_TOTP_ENROLL_ENABLED=false
+MFA_TOTP_VERIFY_ENABLED=false
+```
+
+### Phone MFA
+
+Phone MFA is **disabled by default** (opt-in). It uses the same SMS provider configuration as phone login.
+
+To enable:
+
+```
+MFA_PHONE_ENROLL_ENABLED=true
+MFA_PHONE_VERIFY_ENABLED=true
+```
+
+### Maximum enrolled factors
+
+By default, a user can enroll up to 10 MFA factors. To change this:
+
+```
+MFA_MAX_ENROLLED_FACTORS=5
+```
+
+## Troubleshooting
+
+### OTP expires too quickly
+
+The default `SMS_OTP_EXP` is 60 seconds. Increase it in `.env`:
+
+```
+SMS_OTP_EXP=300
+```
+
+Ensure `GOTRUE_SMS_OTP_EXP: ${SMS_OTP_EXP}` is uncommented in `docker-compose.yml`, then restart:
+
+```sh
+docker compose up -d --force-recreate --no-deps auth
+```
+
+### SMS not being delivered
+
+Check the auth container logs for errors:
+
+```sh
+docker compose logs auth --tail 50
+```
+
+Verify provider credentials reach the container:
+
+```sh
+docker compose exec auth env | grep GOTRUE_SMS
+```
+
+Common causes:
+
+- Provider credentials are in `.env` but the matching `GOTRUE_SMS_*` line is still commented out in `docker-compose.yml`
+- Provider credentials are wrong
+- Phone number format is wrong (use E.164 format: `+1234567890`)
+
+### Variables are configured in `.env` but not working
+
+Configuration variables from `.env` are **not** automatically available inside the container unless there's a matching passthrough definition in `docker-compose.yml`. Check, e.g., for:
+
+```sh
+docker compose exec auth env | grep -E 'GOTRUE_SMS|GOTRUE_MFA'
+```
+
+After changing the configuration environment variables, recreate the Auth service container:
+
+```sh
+docker compose up -d --force-recreate --no-deps auth
+```
+
+### Rate limit errors
+
+If users see "rate limit exceeded" errors, check `SMS_MAX_FREQUENCY` (minimum interval between sends) and the global rate limit `GOTRUE_RATE_LIMIT_SMS_SENT` (default: 30 per hour).
+
+### Additional resources
+
+- [Multi-Factor Authentication (Phone)](/docs/guides/auth/auth-mfa/phone)
+- [Multi-Factor Authentication (TOTP)](/docs/guides/auth/auth-mfa/totp)
+- [Auth server on GitHub](https://github.com/supabase/auth) (check README and `example.env`)
diff --git a/apps/docs/content/guides/self-hosting/self-hosted-proxy-https.mdx b/apps/docs/content/guides/self-hosting/self-hosted-proxy-https.mdx
new file mode 100644
index 0000000000000..25723468ba838
--- /dev/null
+++ b/apps/docs/content/guides/self-hosting/self-hosted-proxy-https.mdx
@@ -0,0 +1,228 @@
+---
+title: 'Configure Reverse Proxy and HTTPS'
+description: 'Set up a reverse proxy with HTTPS for self-hosted Supabase.'
+subtitle: 'Set up a reverse proxy with HTTPS for self-hosted Supabase.'
+---
+
+HTTPS is required for production self-hosted Supabase deployments. This guide covers two production approaches using a reverse proxy in front of self-hosted Supabase API gateway, plus a self-signed certificate option for development environment.
+
+## Before you begin
+
+You need:
+
+- A working self-hosted Supabase installation. See [Self-Hosting with Docker](/docs/guides/self-hosting/docker).
+- A domain name with DNS pointing to your server's public IP address (to obtain Let's Encrypt certificate).
+- Ports 80 and 443 open.
+
+## Set up HTTPS
+
+Below are two options for adding a reverse proxy with automatic HTTPS in front of your self-hosted Supabase: **Caddy** (simpler, zero-config TLS) and **Nginx + Let's Encrypt** (more control over proxy settings). Both sit in front of Kong and terminate TLS, so internal traffic stays on HTTP.
+
+
+
+If you already run [HAProxy](https://www.haproxy.com/), [Traefik](https://traefik.io/), [Nginx Proxy Manager](https://nginxproxymanager.com/), or another reverse proxy for your infrastructure, you can use it instead of Caddy or Nginx above. The key requirements are:
+
+- Proxy to Kong on port `8000` (or `:8000` if the proxy runs outside the Docker network)
+- Enable WebSocket support (required for Realtime)
+- Proxy traffic to Storage directly to the container, bypassing Kong
+- Add `X-Forwarded` headers to all requests
+- Comment out Kong's host port bindings in `docker-compose.yml` if the proxy runs in the same Docker network
+- Update `SUPABASE_PUBLIC_URL`, `API_EXTERNAL_URL`, and `SITE_URL` in `.env` to your HTTPS URL
+
+
+
+### Step 1: Remove public port bindings for API gateway
+
+Comment out Kong's host port mappings in `docker-compose.yml` so that it's not exposed to the Internet:
+
+```yaml
+kong:
+ # ...
+ ports:
+ # - ${KONG_HTTP_PORT}:8000/tcp
+ # - ${KONG_HTTPS_PORT}:8443/tcp
+```
+
+Kong remains accessible to other containers on the internal Docker network.
+
+### Step 2: Update environment variables
+
+Update the URL configuration in your `.env` file to use your HTTPS domain:
+
+```
+SUPABASE_PUBLIC_URL=https://
+API_EXTERNAL_URL=https://
+SITE_URL=https://
+```
+
+Change the following to your domain name and a **valid** email address:
+
+```
+PROXY_DOMAIN=your-domain.example.com
+CERTBOT_EMAIL=admin@your-domain.example.com
+```
+
+### Step 3: Start the reverse proxy
+
+Pick one of the options below and use the corresponding Docker Compose overlay.
+
+
+
+[Caddy](https://caddyserver.com/) automatically provisions and renews Let's Encrypt TLS certificates with zero configuration. It also handles HTTP-to-HTTPS redirects, WebSocket upgrades, and HTTP/2 and HTTP/3 out of the box.
+
+Start Caddy by using the pre-configured `docker-compose.caddy.yml` overlay:
+
+```sh
+docker compose -f docker-compose.yml -f docker-compose.caddy.yml up -d
+```
+
+Caddy configuration is in `volumes/proxy/caddy/Caddyfile`.
+
+
+
+
+This option uses a 3rd party Nginx Docker image ([`jonasal/nginx-certbot`](https://github.com/JonasAlfredsson/docker-nginx-certbot)), which includes Certbot for automatic Let's Encrypt certificate issuance and renewal in a single container.
+
+Start Nginx by using the pre-configured `docker-compose.nginx.yml` overlay:
+
+```sh
+docker compose -f docker-compose.yml -f docker-compose.nginx.yml up -d
+```
+
+Nginx configuration template is in `volumes/proxy/nginx/supabase-nginx.conf.tpl`. On container startup, `${NGINX_SERVER_NAME}` is substituted using the environment variable from the `.env` file. The [`jonasal/nginx-certbot`](https://github.com/JonasAlfredsson/docker-nginx-certbot) image reads the resolved `server_name` to determine which domain to request a Let's Encrypt certificate for.
+
+HTTP-to-HTTPS redirects are handled automatically by the `jonasal/nginx-certbot` image.
+
+
+
+
+### Step 4: Verify HTTPS connection
+
+```sh
+curl -I https:///auth/v1/
+```
+
+You should receive a `401` response confirming you could connect to Auth.
+
+## Self-signed certificates (development only)
+
+
+
+Self-signed certificates trigger browser warnings and are rejected by most OAuth providers. Use this approach only in development environment or internal networks.
+
+
+
+For development or internal networks where you cannot use Let's Encrypt, you can configure Kong to serve HTTPS directly using self-signed certificates.
+
+### Step 1: Generate a self-signed certificate
+
+Change `` in the example below, and create certificates with `openssl`:
+
+```sh
+openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
+ -keyout volumes/api/server.key \
+ -out volumes/api/server.crt \
+ -subj "/CN=" && \
+ chmod 640 volumes/api/server.key && \
+ chgrp 65533 volumes/api/server.key
+```
+
+{/* supa-mdx-lint-disable-next-line Rule001HeadingCase */}
+
+### Step 2: Configure Kong for SSL
+
+Comment out Kong's HTTP port mapping in `docker-compose.yml`:
+
+```yaml
+kong:
+ # ...
+ ports:
+ # - ${KONG_HTTP_PORT}:8000/tcp
+```
+
+Uncomment the certificate volume mounts and SSL environment variables in `docker-compose.yml`:
+
+```yaml
+kong:
+ # ... existing configuration ...
+ volumes:
+ - ./volumes/api/kong.yml:/home/kong/temp.yml:ro,z
+ - ./volumes/api/server.crt:/home/kong/server.crt:ro
+ - ./volumes/api/server.key:/home/kong/server.key:ro
+ environment:
+ # ... existing environment variables ...
+ KONG_SSL_CERT: /home/kong/server.crt
+ KONG_SSL_CERT_KEY: /home/kong/server.key
+```
+
+### Step 3: Update configuration variables in `.env`
+
+Edit your `.env` file to use HTTPS with the Kong HTTPS port:
+
+```
+SUPABASE_PUBLIC_URL=https://:8443
+API_EXTERNAL_URL=https://:8443
+SITE_URL=https://:8443
+```
+
+### Step 4: Restart and verify
+
+```sh
+docker compose down && docker compose up -d
+```
+
+```sh
+curl -I -k https://:8443/auth/v1/
+```
+
+The `-k` flag tells curl to accept the self-signed certificate.
+
+## Troubleshooting
+
+### Certificate not issued
+
+If Caddy or Certbot fails to obtain a certificate:
+
+- Verify that ports 80 and 443 are open on your firewall
+- Verify that your domain's DNS A record points to your server's public IP
+- Check proxy logs via `docker logs supabase-caddy` or `docker logs supabase-nginx`
+- Let's Encrypt has [rate limits](https://letsencrypt.org/docs/rate-limits/) - if you hit them, wait before retrying
+
+### WebSocket connection failed
+
+If Realtime subscriptions fail to connect:
+
+- **Caddy** handles WebSocket upgrades automatically - check that Kong is healthy
+- **Nginx** requires explicit `Upgrade` and `Connection` headers on the `/realtime/v1/` location. Verify your `nginx.conf` includes these headers as shown above
+
+### OAuth callback URL mismatch
+
+If OAuth redirects fail with a callback URL error:
+
+- Verify `API_EXTERNAL_URL` in `.env` is set to your HTTPS URL
+- Verify the callback URL registered with your OAuth provider matches `API_EXTERNAL_URL` followed by `/auth/v1/callback`
+- After changing `API_EXTERNAL_URL`, restart all services with `docker compose down && docker compose up -d`
+
+### Mixed content warnings
+
+If the browser console shows mixed content errors:
+
+- Verify `SUPABASE_PUBLIC_URL` is set to your HTTPS URL
+- Verify `SITE_URL` is also set to HTTPS
+- Clear your browser cache after making changes
+
+### ERR_CERT_AUTHORITY_INVALID
+
+This is expected when using self-signed certificates. For production, use Caddy or Nginx with Let's Encrypt. If you need to use self-signed certificates, add the certificate to your system's trust store or use a browser flag to bypass the warning.
+
+## Additional resources
+
+- [Caddy documentation](https://caddyserver.com/docs/)
+- [Nginx documentation](https://nginx.org/en/docs/) (on nginx.org)
+- [docker-nginx-certbot on GitHub](https://github.com/JonasAlfredsson/docker-nginx-certbot)
diff --git a/apps/studio/components/interfaces/Auth/SessionsAuthSettingsForm/SessionsAuthSettingsForm.tsx b/apps/studio/components/interfaces/Auth/SessionsAuthSettingsForm/SessionsAuthSettingsForm.tsx
index f1273b31f1d69..13b450a398848 100644
--- a/apps/studio/components/interfaces/Auth/SessionsAuthSettingsForm/SessionsAuthSettingsForm.tsx
+++ b/apps/studio/components/interfaces/Auth/SessionsAuthSettingsForm/SessionsAuthSettingsForm.tsx
@@ -11,8 +11,8 @@ import NoPermission from 'components/ui/NoPermission'
import { UpgradeToPro } from 'components/ui/UpgradeToPro'
import { useAuthConfigQuery } from 'data/auth/auth-config-query'
import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation'
+import { useCheckEntitlements } from 'hooks/misc/useCheckEntitlements'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
-import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { IS_PLATFORM } from 'lib/constants'
import {
Button,
@@ -83,9 +83,9 @@ export const SessionsAuthSettingsForm = () => {
'custom_config_gotrue'
)
- const { data: organization } = useSelectedOrganizationQuery()
- const isProPlanAndUp = organization?.plan?.id !== 'free'
- const promptProPlanUpgrade = IS_PLATFORM && !isProPlanAndUp
+ const { hasAccess: hasUserSessionsEntitlement, isLoading: isLoadingEntitlements } =
+ useCheckEntitlements('auth.user_sessions')
+ const promptProPlanUpgrade = IS_PLATFORM && !hasUserSessionsEntitlement
const refreshTokenForm = useForm>({
resolver: zodResolver(RefreshTokenSchema),
@@ -182,7 +182,7 @@ export const SessionsAuthSettingsForm = () => {
)
}
- if (isLoading) {
+ if (isLoading || isLoadingEntitlements) {
return (
@@ -304,7 +304,7 @@ export const SessionsAuthSettingsForm = () => {
@@ -328,7 +328,7 @@ export const SessionsAuthSettingsForm = () => {
type="number"
min={0}
{...field}
- disabled={!canUpdateConfig || !isProPlanAndUp}
+ disabled={!canUpdateConfig || !hasUserSessionsEntitlement}
/>
@@ -353,7 +353,7 @@ export const SessionsAuthSettingsForm = () => {
type="number"
{...field}
className="flex-1"
- disabled={!canUpdateConfig || !isProPlanAndUp}
+ disabled={!canUpdateConfig || !hasUserSessionsEntitlement}
/>
diff --git a/apps/studio/components/interfaces/Billing/Usage/UsageWarningAlerts/CPUWarnings.tsx b/apps/studio/components/interfaces/Billing/Usage/UsageWarningAlerts/CPUWarnings.tsx
index 70449d706c430..7c8a8328fbbf2 100644
--- a/apps/studio/components/interfaces/Billing/Usage/UsageWarningAlerts/CPUWarnings.tsx
+++ b/apps/studio/components/interfaces/Billing/Usage/UsageWarningAlerts/CPUWarnings.tsx
@@ -6,12 +6,16 @@ import { DOCS_URL } from 'lib/constants'
import { AlertDescription_Shadcn_, Alert_Shadcn_, Button } from 'ui'
interface CPUWarningsProps {
- isFreePlan: boolean
+ hasAccessToComputeSizes: boolean
upgradeUrl: string
severity?: 'warning' | 'critical' | null
}
-export const CPUWarnings = ({ isFreePlan, upgradeUrl, severity }: CPUWarningsProps) => {
+export const CPUWarnings = ({
+ hasAccessToComputeSizes,
+ upgradeUrl,
+ severity,
+}: CPUWarningsProps) => {
if (severity === 'warning') {
return (
@@ -28,7 +32,7 @@ export const CPUWarnings = ({ isFreePlan, upgradeUrl, severity }: CPUWarningsPro
@@ -52,7 +56,7 @@ export const CPUWarnings = ({ isFreePlan, upgradeUrl, severity }: CPUWarningsPro
diff --git a/apps/studio/components/interfaces/Billing/Usage/UsageWarningAlerts/DiskIOBandwidthWarnings.tsx b/apps/studio/components/interfaces/Billing/Usage/UsageWarningAlerts/DiskIOBandwidthWarnings.tsx
index 000e1b9257302..10a3c7844232d 100644
--- a/apps/studio/components/interfaces/Billing/Usage/UsageWarningAlerts/DiskIOBandwidthWarnings.tsx
+++ b/apps/studio/components/interfaces/Billing/Usage/UsageWarningAlerts/DiskIOBandwidthWarnings.tsx
@@ -4,7 +4,7 @@ import { Admonition } from 'ui-patterns'
// [Joshen] In the future, conditionals should be from resource exhaustion endpoint as single source of truth
interface DiskIOBandwidthWarningsProps {
- isFreePlan: boolean
+ hasAccessToComputeSizes: boolean
hasLatest: boolean
upgradeUrl: string
currentBillingCycleSelected: boolean
@@ -13,7 +13,7 @@ interface DiskIOBandwidthWarningsProps {
}
export const DiskIOBandwidthWarnings = ({
- isFreePlan,
+ hasAccessToComputeSizes,
hasLatest,
currentBillingCycleSelected,
upgradeUrl,
@@ -34,7 +34,7 @@ export const DiskIOBandwidthWarnings = ({