docs(auth-hooks): clarify webhook status codes and document email_change quirk (#38141)

* docs(auth-hooks): clarify webhook status codes and document email_change quirk

- Clarify hook response handling:
  - Note that some hooks do not support 204 responses if they require a body
  - Update retry-able error behavior for 429/503 with explicit 5s budget
  - Add note that Retry-After header is only checked for non-empty value, not parsed

- Expand send-email-hook docs:
  - Add detailed section on `email_change` behavior
  - Explain secure vs non-secure modes and when two emails must be sent
  - Document long-standing quirk where `token_hash` and `token_hash_new`
    are swapped relative to expected naming
  - Extend enum to include `reauthentication`
  - Improve confirmation URL example using URLSearchParams and projectRef

* fix: fix linter error

* fix: pr feedback

* fix: pnpm format

* fix: pr feedback

---------

Co-authored-by: Chris Stockton <chris.stockton@supabase.io>
This commit is contained in:
Chris StocktonandChris Stockton authored and GitHub committed 2025-08-26 08:41:13 -07:00
1 parent db4ae7e326
commit 97a447ac5c
2 files changed
+70 -14

No files matched your search

+25 -11
View File
@@ -313,23 +313,37 @@ Hooks return status codes based on the nature of the response. These status code
| HTTP Status Code | Description | Example Usage |
| ---------------- | ------------------------------------------------------------- | ---------------------------------------------- |
| 200, 202, 204 | Valid response, proceed | Successful processing of the request |
| 429, 503 | Retry-able errors with Retry-after header supported | Temporary server overload or maintenance |
| 403, 400 | Treated as Internal Server Errors and return a 500 Error Code | Malformed requests or insufficient permissions |
| 429, 503 | Retry-able errors | Temporary server overload or maintenance |
Errors are responses which contain status codes 400 and above. On a retry-able error, such as an error with a `429` or `503` status code, HTTP Hooks will attempt up to three retries with a back-off of two seconds.
<Admonition type="note">
`204` Status is not supported by the following hooks which require a response body:
- [Custom Access Token](/docs/guides/auth/auth-hooks/custom-access-token-hook)
- [MFA Verification Attempt](/docs/guides/auth/auth-hooks/mfa-verification-hook)
- [Password Verification Attempt](/docs/guides/auth/auth-hooks/password-verification-hook)
</Admonition>
Errors are responses which contain status codes 400 and above. On a retry-able error, such as an error with a `429` or `503` status code, HTTP Hooks will attempt up to three retries with a back-off of two seconds. We have a time budget of 5s for the entire webhook invocation, including retry requests.
Here's a sample HTTP retry schedule:
| Time Since Start (HH:MM:SS) | Event | Notes |
| --------------------------- | ----------------------- | ------------------------------------------------- |
| 00:00:00 | Initial Attempt | Initial invocation begins. |
| 00:00:05 | Initial Attempt Timeout | Initial invocation must complete. |
| 00:00:07 | Retry Start #1 | After 2 sec delay, first retry begins. |
| 00:00:12 | Retry Timeout #1 | First retry timeout. |
| 00:00:14 | Retry Start #2 | After 2 sec delay, second retry begins. |
| 00:00:19 | Retry Timeout #2 | Second retry timeout. Returns an error on failure |
| Time Since Start (HH:MM:SS) | Event | Notes |
| --------------------------- | --------------------- | -------------------------------------------------------------------------------- |
| 00:00:00 | Initial Attempt | Initial invocation begins. |
| 00:00:02 | Initial Attempt Fails | Initial invocation returns `429` or `503` with non-empty `retry-after` header. |
| 00:00:04 | Retry Start #1 | After 2 sec delay, first retry begins. |
| 00:00:05 | Retry Timeout #1 | First retry times out, exceeded 5 second budget and invocation returns an error. |
Return a retry-able error by attaching a appropriate status code (`429`, `503` ) and a non-empty `retry-after` header
Return a retry-able error by attaching a appropriate status code (`429`, `503`) and a non-empty `retry-after` header
<Admonition type="note">
`Retry-After` Supabase Auth does not fully support the `Retry-After` header as described in RFC7231, we only check if it is a non-empty value such as `true` or `10`. Setting this to your preferred value is fine as a future update may address this.
</Admonition>
```jsx
return new Response(
@@ -17,6 +17,33 @@ Email sending depends on two settings: Email Provider and Auth Hook status.
| Disabled | Enabled | Email Signups Disabled |
| Disabled | Disabled | Email Signups Disabled |
## Email change behavior and token hash mapping
When `email_action_type` is `email_change`, the hook payload can include one or two OTPs and their hashes. This depends on your [Secure Email Change](/dashboard/project/_/auth/providers?provider=Email) setting.
- Secure Email Change enabled: two OTPs are generated, one for the current email (`user.email`) and one for the new email (`user.email_new`). You must send two emails.
- Secure Email Change disabled: only one OTP is generated for the new email. You send a single email.
<Admonition type="note">
Important quirk (backward compatibility):
- `email_data.token_hash_new` = Hash(`user.email`, `email_data.token`)
- `email_data.token_hash` = Hash(`user.email_new`, `email_data.token_new`)
This naming is historical and kept for backward compatibility. Do not assume that the `_new` suffix refers to the new email.
</Admonition>
### What to send
If both `token_hash` and `token_hash_new` are present, send two messages:
- To the current email (`user.email`): use `token` with `token_hash_new`.
- To the new email (`user.email_new`): use `token_new` with `token_hash`.
If only one token/hash pair is present, send a single email. In non-secure mode, this is typically the new email OTP. Use `token` with `token_hash` or `token_new` with `token_hash`, depending on which fields are present in the payload.
**Inputs**
| Field | Type | Description |
@@ -282,7 +309,15 @@ Email sending depends on two settings: Email Provider and Auth Hook status.
},
"email_action_type": {
"type": "string",
"enum": ["signup", "invite", "magiclink", "recovery", "email_change", "email"]
"enum": [
"signup",
"invite",
"magiclink",
"recovery",
"email_change",
"email",
"reauthentication"
]
},
"site_url": {
"type": "string",
@@ -578,6 +613,7 @@ import { readAll } from 'https://deno.land/std/io/read_all.ts'
const postmarkEndpoint = 'https://api.postmarkapp.com/email'
// Replace this with your email
const FROM_EMAIL = 'myemail@gmail.com'
const PROJECT_REF = '<your-project-ref>'
// Email Subjects
const subjects = {
@@ -642,8 +678,14 @@ const templates = {
}
function generateConfirmationURL(email_data) {
// TODO: replace the ref with your project ref
return `https://<ref>.supabase.co/auth/v1/verify?token=${email_data.token_hash}&type=${email_data.email_action_type}&redirect_to=${email_data.redirect_to}`
const baseUrl = `https://${PROJECT_REF}.supabase.co/auth/v1/verify`
const params = new URLSearchParams({
token: email_data.token_hash,
type: email_data.email_action_type,
redirect_to: email_data.redirect_to,
})
return `${baseUrl}?${params.toString()}`
}
Deno.serve(async (req) => {