本页还没有译文
本页 70 段还没译成한국어,下面显示的是英文原文。Configure Auth Hooks
还没翻译译自英文(supabase.com)本页 70 段都还没译
This guide covers the server-side configuration required to enable auth hooks on a self-hosted Supabase instance running with Docker Compose. Auth hooks let you run custom logic at specific points in the authentication flow - for example, adding claims to JWTs, sending SMS through a custom provider, or restricting signups.
Before you begin
You need:
- A working self-hosted Supabase installation. See Self-Hosting with Docker.
- For Postgres function hooks: access to the database service to create functions.
- For HTTP endpoint hooks: a reachable HTTPS endpoint or a local Edge Function.
How hooks work
For hook implementation details (input/output schemas, SQL and HTTP examples), see Auth Hooks.
Supabase Auth can call a hook at specific lifecycle events during the auth flow. Each hook can be configured with the following environment variables:
GOTRUE_HOOK_{HOOK_NAME}_ENABLED: Enable the hook (true/false)GOTRUE_HOOK_{HOOK_NAME}_URI: The hook endpointGOTRUE_HOOK_{HOOK_NAME}_SECRETS: Webhook signing secrets (for HTTP hooks)
| Hook | Hook Name | Description |
|---|---|---|
| Custom Access Token | CUSTOM_ACCESS_TOKEN | Add claims to JWTs before they are issued |
| Send SMS | SEND_SMS | Replace built-in SMS sending with a custom provider |
| Send Email | SEND_EMAIL | Replace built-in email sending with a custom provider |
| Before User Created | BEFORE_USER_CREATED | Run checks or block signups before creating a user |
| MFA Verification | MFA_VERIFICATION_ATTEMPT | Validate MFA attempts (rate limit, brute-force protection) |
| Password Verification | PASSWORD_VERIFICATION_ATTEMPT | Track and limit failed password attempts |
URI schemes
Hooks support two URI schemes:
| Scheme | Format |
|---|---|
pg-functions:// | pg-functions://postgres/<schema>/<function_name> |
http:// or https:// | https://example.com/hook |
Note
Postgres function hooks run inside your database, so there is no network overhead and no need to manage secrets.
Note
http:// URIs are only allowed for localhost, 127.0.0.1, ::1, and host.docker.internal hostnames.
Step-by-step: Postgres function hook
This example enables the Custom Access Token hook using a Postgres function that adds a user_role claim to the JWT.
Step 1: Create the Postgres function
You can execute the following SQL from the Supabase Dashboard SQL Editor, or by connecting to your database using a Postgres client such as psql.
This example reads roles from a user_roles table, so create that table first. If the table is missing, the hook errors and every sign-in fails.
create table if not exists public.user_roles (
user_id uuid not null references auth.users
on delete cascade,
role text not null,
primary key (user_id)
);Then create the hook function:
create or replace function public.custom_access_token_hook(event jsonb)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
claims jsonb;
user_role text;
begin
claims := event->'claims';
-- Example: look up a custom role from a user_roles table
select role into user_role
from public.user_roles
where user_id = (event->>'user_id')::uuid;
if user_role is not null then
claims := jsonb_set(
claims, '{user_role}', to_jsonb(user_role)
);
end if;
-- Return the modified claims
return jsonb_build_object('claims', claims);
end;
$$;
-- Grant execute permission to supabase_auth_admin
grant execute on function public.custom_access_token_hook
to supabase_auth_admin;
-- Grant schema access to supabase_auth_admin (usually already granted by default)
grant usage on schema public to supabase_auth_admin;
-- Revoke from public and other roles
revoke execute on function public.custom_access_token_hook
from authenticated, anon, public;Step 2: Update docker-compose.yml
Update the auth service environment: block:
services:
auth:
environment:
# ... existing variables ...
GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_ENABLED: 'true' # 👈 enabling the hook is required
GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_URI: 'pg-functions://postgres/public/custom_access_token_hook'Step 3: Relaunch the auth service
sh run.sh recreate authStep 4: Verify the custom claim
Give a user a role so the hook has something to add. Replace the UUID with a real user ID from auth.users:
insert into public.user_roles (user_id, role)
values ('00000000-0000-0000-0000-000000000000', 'admin');Sign in as that user and decode the JWT. If the hook ran successfully, the user_role claim is present. A user with no matching row in user_roles still signs in, but without the claim. If something doesn't work, check the auth logs:
docker compose logs auth --tail 20Step-by-step: HTTP endpoint hook
This example enables the Send SMS hook using an Edge Function.
Note
The Send SMS hook only fires when Auth sends an OTP. Make sure phone auth is enabled (GOTRUE_EXTERNAL_PHONE_ENABLED=true) and automatic phone confirmation is off (GOTRUE_SMS_AUTOCONFIRM=false). When GOTRUE_SMS_AUTOCONFIRM is on, signups are confirmed without an OTP, so the hook never runs.
Step 1: Create the Edge Function
Create volumes/functions/send_sms/index.ts:
import { Webhook } from 'https://esm.sh/standardwebhooks@1.0.0'
// Note: this example assumes a single secret. If you use multiple secrets (e.g. "v1,whsec_new|v1,whsec_old"), split on '|'
// and try each secret in turn until wh.verify() succeeds.
const hookSecret = Deno.env.get('SEND_SMS_HOOK_SECRET')?.replace('v1,whsec_', '')
Deno.serve(async (req) => {
if (req.method !== 'POST') {
return new Response('not allowed', { status: 400 })
}
if (!hookSecret) {
console.error('SEND_SMS_HOOK_SECRET environment variable not provided')
return new Response('{}', { status: 500 })
}
// Verify the webhook signature
const payload = await req.text()
const headers = Object.fromEntries(req.headers)
const wh = new Webhook(hookSecret)
const { user, sms } = wh.verify(payload, headers)
// Send SMS using your provider
// ... your sms sending logic here ...
return new Response(JSON.stringify({}), {
headers: { 'Content-Type': 'application/json' },
})
})Step 2: Generate a webhook secret
Generate a secret using the following command:
echo "v1,whsec_$(openssl rand -base64 32)"Copy the output of this command, for example: v1,whsec_abc123....
Step 3: Update .env file
Add the following environment variables to your .env file:
SEND_SMS_HOOK_URI=http://host.docker.internal:8000/functions/v1/send_sms
SEND_SMS_HOOK_SECRET=YOUR_GENERATED_SECRET_HERE # Paste the secret generated in the last step hereStep 4: Update docker-compose.yml
Add the following environment variables to the auth and functions services. For brevity, only the updated fields are shown.
services:
auth:
environment:
# ... existing variables ...
GOTRUE_HOOK_SEND_SMS_ENABLED: 'true'
GOTRUE_HOOK_SEND_SMS_URI: ${SEND_SMS_HOOK_URI}
GOTRUE_HOOK_SEND_SMS_SECRETS: ${SEND_SMS_HOOK_SECRET}
extra_hosts: # 👈 required so the container can resolve host.docker.internal
- 'host.docker.internal:host-gateway'
functions:
environment:
# ... existing variables ...
SEND_SMS_HOOK_SECRET: ${SEND_SMS_HOOK_SECRET}Step 5: Relaunch auth and functions services
sh run.sh recreate auth functionsStep 6: Verify the hook fires
Trigger an SMS authentication event and confirm that the hook executes successfully.
If something doesn't work, check the auth and functions logs:
docker compose logs auth --tail 20
docker compose logs functions --tail 20Webhook secrets
HTTP hooks use the Standard Webhooks specification for payload signing.
Generating a secret
echo "v1,whsec_$(openssl rand -base64 32)"Secret format
- Symmetric:
v1,whsec_[base64]{32-88 characters}
Key rotation
Separate multiple secrets with | to rotate keys without downtime. For example:
SEND_EMAIL_HOOK_SECRET=v1,whsec_new-secret|v1,whsec_old-secretservices:
auth:
environment:
# ... existing variables ...
GOTRUE_HOOK_SEND_EMAIL_ENABLED: 'true'
GOTRUE_HOOK_SEND_EMAIL_URI: 'https://example.com'
GOTRUE_HOOK_SEND_EMAIL_SECRETS: ${SEND_EMAIL_HOOK_SECRET}The Auth service signs each request with all configured secrets, so receivers can verify against either. Once all clients accept the new secret, remove the old one.
Note
Postgres function hooks (pg-functions:// URIs) do not require secrets - they run directly inside the database.
Troubleshooting
Hook not firing
- Check that
GOTRUE_HOOK_{HOOK_NAME}_ENABLEDis set to"true"(as a string) indocker-compose.yml - Verify the variable reaches the container:
sh run.sh printenv auth | grep GOTRUE_HOOK - Remember:
.envvariables do not reach the container unless passed through indocker-compose.yml
pg-functions:// URI errors
The URI format must be exactly pg-functions://postgres/<schema>/<function_name>:
- Use
postgresas the host by convention. The host segment is not validated. - Schema and function name must be valid Postgres identifiers
- The function must exist and be granted to
supabase_auth_admin
HTTP hook returns errors
Check auth logs for details:
docker compose logs auth --tail 20Common causes:
- The endpoint is not reachable from the auth container
http://is only allowed forlocalhost,127.0.0.1,::1, andhost.docker.internal
Webhook secret format mismatch
Secrets must match the Standard Webhooks format:
- Symmetric:
v1,whsec_[base64](32-88 base64 characters after the prefix) - No spaces or newlines in the secret string
- Generate with:
echo "v1,whsec_$(openssl rand -base64 32)"
Permission denied on Postgres function
The supabase_auth_admin role needs execute on the function. By default every role inherits execute from the public role, but the setup in Step 1 revokes it from public, so you must grant it back to supabase_auth_admin explicitly. Otherwise sign-in fails with 500: Error running hook URI, and the auth logs show a permission-denied error:
grant execute on function public.your_hook_function
to supabase_auth_admin;The role also needs usage on the schema. On a default install it already has this through the built-in grant on the public schema, but grant it explicitly if your database has revoked usage from public:
grant usage on schema public to supabase_auth_admin;SMS OTP expiry is too short
Refer to OTP Settings Docs