Your domain in auth links
Many auth providers send verification, magic-link, reset and invite emails whose button points at the provider’s own host, not your app. A careful reader sees an address they have never heard of. Some spam filters see the same thing. And some link styles only work in the browser that started the flow. Here is how to put your own domain in the link, provider by provider, and which fixes are free.
Why it matters
- A link to
abcdxyz.supabase.coin an email from your product looks like phishing to a careful person. - Filters weigh links to domains that don’t match the sender, so the email is more likely to land in spam.
- Some link styles (Supabase’s PKCE links, Clerk’s email links by default) only complete in the browser that started the flow. Open the email on a phone and it fails.
Do this first
| Provider | Default link host | Free fix? | Effort |
|---|---|---|---|
| Supabase | <project-ref>.supabase.co/auth/v1/verify | Yes: edit the email templates and add one route | About 30 minutes |
| Firebase Auth | <project>.firebaseapp.com/__/auth/action | Yes: custom action URL; no paid plan named in the docs | About an hour, plus a handler page |
| Auth0 | <tenant>.auth0.com | Yes: the Free plan lists 1 custom domain (credit card verification required) | DNS plus a toggle |
| Clerk | Development instances use an accounts.dev domain | Yes: a production instance on your own domain is on the free Hobby plan | DNS records, up to 48 hours |
| Amazon Cognito | <prefix>.auth.<region>.amazoncognito.com | Partly: a verification code needs no link; a custom domain is more work | Code: minutes. Domain: hours |
| WordPress | Your own site (already correct) | Nothing to fix, unless a plugin rewrites links | Check one setting |
Supabase
Default host: https://<project-ref>.supabase.co/auth/v1/verify?…, the {{ .ConfirmationURL }} in each template. With the PKCE flow the code verifier lives in the browser that started the flow, so Supabase’s own docs say the exchange has to finish on the same browser and device.
Free fix: link to your own route with the token hash, and verify it on your server. Free. Because your server calls verifyOtp with the hash, no browser-held verifier is involved, so the link also opens on another device. (That second point is our reading of how the two flows differ; Supabase’s docs state the PKCE restriction, not this benefit.)
Steps. In the dashboard, open Authentication, then Email Templates. In each template, replace {{ .ConfirmationURL }} with a link like this:
<h2>Confirm your email address</h2>
<p>
<a href="{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email&next={{ .RedirectTo }}">
Confirm email address
</a>
</p>Use the type that matches the template:
Confirm signup type=email
Magic link type=email
Invite user type=invite
Change email type=email_change
Reset password type=recovery{{ .SiteURL }} is the Site URL in your Authentication URL settings, so set it to your real domain. Then add the route that exchanges the hash for a session (Next.js App Router, with @supabase/ssr):
// app/auth/confirm/route.ts
import { type EmailOtpType } from '@supabase/supabase-js';
import { redirect } from 'next/navigation';
import { type NextRequest } from 'next/server';
import { createClient } from '@/utils/supabase/server';
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const token_hash = searchParams.get('token_hash');
const type = searchParams.get('type') as EmailOtpType | null;
const next = searchParams.get('next') ?? '/';
if (token_hash && type) {
const supabase = await createClient(); // the @supabase/ssr server client
const { error } = await supabase.auth.verifyOtp({ type, token_hash });
if (!error) redirect(next);
}
redirect('/auth/auth-code-error');
}Paid route: the Custom Domains add-on puts your domain on Supabase’s own auth endpoints (for example auth.yourdomain.com instead of the supabase.co host). It needs a paid plan: Supabase’s pricing page lists it at $10 per domain per month per project, on Pro and above, as of October 2026. Supabase’s custom-domains page doesn’t say outright that email links switch to it, so test one email before relying on it.
Source: supabase.com/docs/guides/auth/server-side/email-based-auth-with-pkce-flow-for-ssr
Firebase Auth
Default host: https://<project>.firebaseapp.com/__/auth/action?mode=…&oobCode=….
Free fix: point the templates at your own action handler page. The Firebase docs name no paid plan for it. No paid plan stated.
- Build a page on your domain that reads
mode,oobCode,apiKey,continueUrlandlangfrom the URL and finishes the action with the Firebase JS SDK (verifyPasswordResetCodethenconfirmPasswordReset;applyActionCodefor email verification and recovery). - In the Firebase console go to Security, then Authentication, then the Templates tab. Edit each template with the pencil icon, choose “customize action URL”, and enter your page’s URL. Firebase appends
modeandoobCode.
Separately, Firebase can send the emails’ From address and action links from your domain: on the same tab choose “customize domain” and add the TXT and CNAME records it shows (verification can take up to 24 hours). Its docs don’t name a paid plan for this either; multi-tenant projects need Identity Platform.
Auth0
Default host: your tenant domain, https://<tenant>.auth0.com.
Free fix: a custom domain. Auth0’s pricing page lists 1 custom domain on the Free plan and custom domains included on the paid plans (Essentials, Professional, Enterprise), with credit card verification required on every plan. Free, with a card on file.
- In the Auth0 Dashboard go to Branding, then Custom Domains. Add your domain and choose a certificate type (Auth0-managed or self-managed).
- Add the DNS record Auth0 shows (a CNAME for Auth0-managed certificates) and verify.
- Still under Custom Domains, turn on “Use Custom Domain in Emails”. Auth0’s docs say links in verification and password change emails use your custom domain once this is enabled.
Use the custom domain’s URL when your app calls Auth0, or the flow keeps running on the tenant domain. We didn’t find Auth0’s exact template variable for this in the docs we read.
Source: auth0.com/docs/customize/custom-domains
Source: auth0.com/pricing
Clerk
Default host: a development instance uses a Clerk development domain ending in accounts.dev (for example accounts.<name>.clerk.accounts.dev), and its emails come from @accounts.dev. A production instance uses your own domain.
Free fix: deploy a production instance. Clerk’s pricing page lists a custom domain on the free Hobby plan. Clerk’s docs add that deploying with paid features turned on asks you to upgrade to Pro. Free.
- Own a domain you can edit DNS for. In the Clerk Dashboard, create the production instance and add that domain.
- Add the DNS records shown on the Domains page. They cover session handling and domain-verified email (SPF and DKIM). Clerk says they can take up to 48 hours to propagate.
- Switch your app to the production keys (
pk_live_…andsk_live_…) and click Deploy certificates.
We could not confirm from Clerk’s docs the exact host inside the email link on a production instance, so open a real email after you deploy. Also, Clerk’s email links are on “Require the same device and browser” by default, which is the same any-browser problem; turn it off only if you accept the trade-off.
Source: clerk.com/docs/guides/development/deployment/production
Source: clerk.com/pricing
Amazon Cognito
Default host: your user pool domain, for example <prefix>.auth.<region>.amazoncognito.com. Cognito’s sign-up link looks like https://<your user pool domain>/confirmUser/?client_id=…&confirmation_code=…. Per its docs, only the sign-up and resend-code emails can carry a link; password reset and attribute updates use a code.
Cheapest fix: use Code as the verification type. There is no link to point at the wrong host, and the email only shows a code. Free. In the console: your user pool, Messages (Message templates), edit the Verification message, and set Verification type to Code.
If you want a link: build it yourself in a custom message Lambda trigger. It needs EmailSendingAccount set to DEVELOPER (that is, sending through Amazon SES), because Cognito rejects a custom email body otherwise. Lambda and SES have their own AWS charges; we didn’t look up the numbers.
// Custom message Lambda: sign-up email with your own link
export const handler = async (event) => {
if (event.triggerSource === 'CustomMessage_SignUp') {
const code = event.request.codeParameter; // "{####}", Cognito fills in the real code
const user = encodeURIComponent(event.userName);
const link = `https://app.yourdomain.com/verify?user=${user}&code=${code}`;
event.response.emailSubject = 'Confirm your email';
event.response.emailMessage = `<p><a href="${link}">Confirm your email</a></p>`;
}
return event;
};Your page then confirms the user with the code through the Cognito API. A message template in the console can override what the Lambda sets, so clear it if your text doesn’t show.
Custom domain: Cognito also supports your own domain for the sign-in pages. It needs a domain you own with an A record on its parent, an ACM certificate in us-east-1, and a DNS alias to the CloudFront target Cognito gives you. In the console: Branding, Domain, Actions, Create custom domain. The docs don’t say whether it changes the host inside email links, and they don’t state a price.
Source: docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-custom-message.html
Source: docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-add-custom-domain.html
WordPress
Default host: your own site. WordPress builds the password reset link from the site address, as wp-login.php?action=rp&key=…&login=… on your domain. Nothing to fix here. Free.
The thing that breaks it is click tracking. Some SMTP plugins and sending services (WP Mail SMTP added click tracking in version 2.9; Mailgun has a click-tracking setting) rewrite every link to a tracking domain. The reset link then points at a host that isn’t yours, and a certificate mismatch can add a browser warning.
- In your SMTP plugin or sending service, turn click tracking off for transactional mail: password reset, verification and invites.
- If you want tracking on marketing mail, use a tracking domain on your own subdomain, with its own certificate.
- Send yourself a reset email and check where the button really goes.
Our source for the default link is WordPress’s developer reference; the click-tracking notes come from the plugin and service pages below, not from WordPress core.
Source: developer.wordpress.org/reference/hooks/retrieve_password_message/
Source: wpmailsmtp.com/announcing-email-open-and-click-tracking/
Then re-run your signup test
Change one provider setting, then run the signup and reset flows again. Flowproof reads the email that really arrives, so you can see where the link points and whether it opens. See how a test run works, and sender setup for the DNS records that prove the email is yours.