Skip to content

fix(invite): mirror /auth/confirm under [locale]/(marketing) — next-intl rewrite was 404ing#130

Merged
gkhngyk merged 3 commits into
mainfrom
fix/invite-auth-confirm-locale-mirror
May 30, 2026
Merged

fix(invite): mirror /auth/confirm under [locale]/(marketing) — next-intl rewrite was 404ing#130
gkhngyk merged 3 commits into
mainfrom
fix/invite-auth-confirm-locale-mirror

Conversation

@gkhngyk

@gkhngyk gkhngyk commented May 30, 2026

Copy link
Copy Markdown
Contributor

What

PR #129 added the new /auth/confirm route at the app-root level only:

web/src/app/auth/confirm/route.ts

After merge + deploy the route still returned 404. Diagnosed via response headers:

Path x-matched-path x-vercel-cache Status
/auth/callback /[locale]/auth/callback MISS 307 ✓
/auth/confirm /404 HIT 404 ✗
/en/auth/confirm (redirects to /auth/confirm) 307 → 404

next-intl middleware rewrites /auth/* to /[locale]/auth/* for every request. /auth/callback works because there's already a [locale]/(marketing)/auth/callback/route.ts for the rewrite to land on. We didn't have one for confirm.

Scope

  • New web/src/app/[locale]/(marketing)/auth/confirm/route.ts — same verifyOtp({ type, token_hash }) logic as the root sibling, just at the path next-intl actually resolves to.
  • Kept the root-level /auth/confirm file around to stay consistent with how the callback pair is shaped (root + locale, even though only locale is hit in practice).

Test plan

  1. Merge → wait for Vercel deploy
  2. curl -I https://app.ansvisor.com/auth/confirm should now show x-matched-path: /[locale]/auth/confirm and a 302/307 (route runs, falls into the missing-params branch with no token_hash)
  3. Update the Supabase Invite User email template to:
    {{ .RedirectTo }}&token_hash={{ .TokenHash }}&type=invite
  4. Fresh invite → click → land on /invite/<token> signed in → set password → join → sign back in

gkhngyk added 3 commits May 30, 2026 19:16
…-flow tokens, not PKCE codes

The /auth/callback hop added in #127 only works for PKCE OAuth flows
where the browser holds the code verifier. Admin-initiated invite
emails (supabase.auth.admin.inviteUserByEmail) have no PKCE verifier
on the recipient's browser — Supabase's verify endpoint returns the
session as URL hash params (#access_token=...&refresh_token=...) which
server components can't read. /auth/callback sees no ?code=, redirects
to /sign-in?error=auth_callback_failed, and the user falls into the
old broken /sign-up dance again.

Canonical fix is the OTP-token-hash flow Supabase documents for SSR:

- New /auth/confirm route handler reads ?token_hash + ?type + ?redirect_to,
  calls supabase.auth.verifyOtp({ type, token_hash }), and 302s to the
  destination once the session cookie is set server-side. Works for
  every Supabase email type (invite, signup, recovery, magiclink,
  email_change) — the type comes from the template, not from us.
- team.ts inviteLink is now plain ${appUrl}/invite/<token> again. The
  auth hop is no longer in the redirect; it's in the email template's
  link itself.

Required ops step (manual, not in code): the Supabase project's Auth →
Email Templates → 'Invite user' template needs the link changed from
{{ .ConfirmationURL }} to:

  {{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=invite&redirect_to={{ .RedirectTo }}

Same swap recommended for signup / recovery / magiclink templates so
every email auth flow goes through the same SSR-friendly path. Until
that's done the invite link still hits the old default URL and the
fix doesn't take effect.
…template covers dev + prod

Previous attempt put /auth/confirm under {{ .SiteURL }} in the email
template. Supabase's Site URL is one project-wide value (production
https://app.ansvisor.com here), so the same template can't drive
both prod testing and a localhost dev loop — the link always points
at prod regardless of NEXT_PUBLIC_APP_URL.

Switch the redirectTo we pass to inviteUserByEmail to be the full
/auth/confirm URL with our next path baked in:

  ${appUrl}/auth/confirm?next=/invite/<token>

Template now uses {{ .RedirectTo }} as the prefix and only appends
the OTP params:

  {{ .RedirectTo }}&token_hash={{ .TokenHash }}&type=invite

Because appUrl resolves to whichever NEXT_PUBLIC_APP_URL is in the
current environment, the link goes to localhost in dev and prod in
prod — without changing Supabase project settings between the two.
…intl can resolve it

PR #129 added /auth/confirm at the root app directory only. next-intl
middleware rewrites every /auth/* request to /[locale]/auth/*, and the
deployed bundle had no matching file under [locale]/auth/confirm — so
the request landed on the framework 404 page.

The existing /auth/callback works because there's already a
[locale]/(marketing)/auth/callback/route.ts that the rewrite resolves
to. Add the equivalent mirror for confirm so the same routing rules
have a destination.

Verified via response headers — /auth/callback shows
`x-matched-path: /[locale]/auth/callback`, /auth/confirm before this
change matched /404 with x-vercel-cache HIT. Keeping the root
/auth/confirm/route.ts around to stay consistent with the callback
sibling pattern; it's never hit but matches the existing convention.
@gkhngyk gkhngyk merged commit 889a321 into main May 30, 2026
4 checks passed
gkhngyk added a commit that referenced this pull request May 31, 2026
Bundles the in-product Agent epic (#120, #121), the BYOK rollout on
cloud, the MCP toolkit expansion (#109, #110, #116, #117, #118), the
insights performance fix (#114), and the invite-accept flow rebuild
(#127, #129, #130) into a tagged release.

See CHANGELOG.md for the full notes.
gkhngyk added a commit that referenced this pull request May 31, 2026
Bundles the in-product Agent epic (#120, #121), the BYOK rollout on
cloud, the MCP toolkit expansion (#109, #110, #116, #117, #118), the
insights performance fix (#114), and the invite-accept flow rebuild
(#127, #129, #130) into a tagged release.

See CHANGELOG.md for the full notes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant