Skip to main content

Send Email Hook

还没翻译译自英文(supabase.com)本页 40 段都还没译

The Send Email Hook replaces Supabase's built-in email sending. You can use this hook to:

  • Send emails using your own email provider
  • Add internationalization or custom logic
  • Fall back to another provider if your primary one fails

Inputs

FieldTypeDescription
userUserThe user account taking the action
emailobjectMetadata specific to the email sending process

JSONJSON Schema

{
  "user": {
    "id": "8484b834-f29e-4af2-bf42-80644d154f76",
    "aud": "authenticated",
    "role": "authenticated",
    "email": "valid.email@supabase.io",
    "phone": "",
    "app_metadata": {
      "provider": "email",
      "providers": ["email"]
    },
    "user_metadata": {
      "email": "valid.email@supabase.io",
      "email_verified": false,
      "phone_verified": false,
      "sub": "8484b834-f29e-4af2-bf42-80644d154f76"
    },
    "identities": [
      {
        "identity_id": "bc26d70b-517d-4826-bce4-413a5ff257e7",
        "id": "8484b834-f29e-4af2-bf42-80644d154f76",
        "user_id": "8484b834-f29e-4af2-bf42-80644d154f76",
        "identity_data": {
          "email": "valid.email@supabase.io",
          "email_verified": false,
          "phone_verified": false,
          "sub": "8484b834-f29e-4af2-bf42-80644d154f76"
        },
        "provider": "email",
        "last_sign_in_at": "2024-05-14T12:56:33.824231484Z",
        "created_at": "2024-05-14T12:56:33.824261Z",
        "updated_at": "2024-05-14T12:56:33.824261Z",
        "email": "valid.email@supabase.io"
      }
    ],
    "created_at": "2024-05-14T12:56:33.821567Z",
    "updated_at": "2024-05-14T12:56:33.825595Z",
    "is_anonymous": false
  },
  "email_data": {
    "token": "305805",
    "token_hash": "7d5b7b1964cf5d388340a7f04f1dbb5eeb6c7b52ef8270e1737a58d0",
    "redirect_to": "http://localhost:3000/",
    "email_action_type": "signup",
    "site_url": "http://localhost:9999",
    "token_new": "",
    "token_hash_new": "",
    "old_email": "",
    "old_phone": "",
    "provider": "",
    "factor_type": ""
  }
}

Outputs

  • No outputs are required. An empty response with a status code of 200 is taken as a successful response.

Email sending behavior

Email sending depends on two settings: Email Provider and Auth Hook status.

Email ProviderAuth HookResult
EnabledEnabledAuth Hook handles email sending (SMTP not used)
EnabledDisabledSMTP handles email sending (custom if configured, default otherwise)
DisabledEnabledEmail signups disabled
DisabledDisabledEmail 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 setting.

  • Secure Email Change enabled: two OTPs are generated, one for the current email (user.email) and one for the new email (user.new_email). You must send two emails.
  • Secure Email Change disabled: only one OTP is generated for the new email. You send a single email.

Counterintuitive field naming

The token hash field names are reversed due to backward compatibility. Pay careful attention to which token/hash pair goes with which email address:

  • token_hash_new → use with the current email address (user.email) and token
  • token_hash → use with the new email address (user.new_email) and token_new

Do not assume the _new suffix refers to the new email address.

What to send

When Secure Email Change is enabled (both token/hash pairs present):

  • Send to current email address (user.email): use token with token_hash_new
  • Send to new email address (user.new_email): use token_new with token_hash

When Secure Email Change is disabled (only one token/hash pair present):

  • Send a single email to the new email address. Use token with token_hash or token_new with token_hash, depending on which fields are present in the payload.

SQLHTTP

Queue Email Messages

Your company uses a worker to manage all emails related jobs. For performance reasons, the messaging system sends emails in batches via a job queue. Instead of sending a message immediately, messages are queued and sent in periodic intervals via pg_cron.

Create a table to store jobs

create table job_queue (
  job_id uuid primary key default gen_random_uuid(),
  job_data jsonb not null,
  created_at timestamp default now(),
  status text default 'pending',
  priority int default 0,
  retry_count int default 0,
  max_retries int default 2,
  scheduled_at timestamp default now()
);

Create the hook

create or replace function send_email(event jsonb) returns jsonb as $$
declare
    job_data jsonb;
    scheduled_time timestamp;
    priority int;
begin
    -- Extract email details from the event JSON
    job_data := jsonb_build_object(
        'email_action_type', event->'email_data'->>'email_action_type',
        'token_hash', event->'email_data'->>'token_hash',
        'token', event->'email_data'->>'token',
        'email', event->'user'->>'email'
    );

    -- Calculate the nearest 5-minute window for scheduled_time
    scheduled_time := date_trunc('minute', now()) + interval '5 minute' * floor(extract('epoch' from (now() - date_trunc('minute', now())) / 60) / 5);

    -- Assign priority dynamically (example logic: higher priority for earlier scheduled time)
    priority := extract('epoch' from (scheduled_time - now()))::int;

    insert into public.job_queue (job_data, priority, scheduled_at, max_retries)
    values (job_data, priority, scheduled_time, 2);

    return '{}'::jsonb;
end;
$$ language plpgsql;

grant all
  on table public.job_queue
  to supabase_auth_admin;

revoke all
  on table public.job_queue
  from authenticated, anon;

Create a function to periodically run and dequeue all jobs

create or replace function dequeue_and_run_jobs() returns void as $$
declare
    job record;
begin
    for job in
        select * from job_queue
        where status = 'pending'
          and scheduled_at <= now()
        order by priority desc, created_at
        for update skip locked
    loop
        begin
            -- add job processing logic here.
            -- for demonstration, we'll just update the job status to 'completed'.
            update job_queue
            set status = 'completed'
            where job_id = job.job_id;

        exception when others then
            -- handle job failure and retry logic
            if job.retry_count < job.max_retries then
                update job_queue
                set retry_count = retry_count + 1,
                    scheduled_at = now() + interval '1 minute'  -- delay retry by 1 minute
                where job_id = job.job_id;
            else
                update job_queue
                set status = 'failed'
                where job_id = job.job_id;
            end if;
        end;
    end loop;
end;
$$ language plpgsql;

grant execute
  on function public.dequeue_and_run_jobs
  to supabase_auth_admin;

revoke execute
  on function public.dequeue_and_run_jobs
  from authenticated, anon;

Configure pg_cron to run the job on an interval. You can use a tool like crontab.guru to check that your job is running on an appropriate schedule. Ensure that pg_cron is enabled under Database > Extensions

select
  cron.schedule(
    '* * * * *', -- this cron expression means every minute.
    'select dequeue_and_run_jobs();'
  );