What is wired · 3 min
Authentication
Magic links and OAuth on Supabase Auth, and the two failures that cost a day each if you meet them fresh.
Sessions live in cookies that the server can read, which is what makes
requireUser() work in a Server Component without a loading state.
import { requireUser, currentUser } from "@/lib/auth";
// In a page that makes no sense signed out. Redirects if there is no session.
const user = await requireUser();
// Where signed out is a normal state, the nav for example.
const maybe = await currentUser();currentUser is wrapped in React's cache, so a layout and the page inside it
share one call to the auth server rather than making two.
The magic link trap
Supabase's default email templates use {{ .ConfirmationURL }}, which returns
the session in a hash fragment. A fragment is never sent to the server. Your
callback route sees an empty query string, creates no session, and bounces the
user back to sign-in with no error to show them. It looks like the link is
broken. It is not: the session went to the browser and nothing read it.
Rewrite all three templates, confirm signup, magic link and email change, to send a token hash to your own callback:
{{ .SiteURL }}/auth/callback?token_hash={{ .TokenHash }}&type=magiclink
The cookie trap
In a route handler, mutating the cookie store from next/headers does not
carry onto a NextResponse.redirect. You verify the token, you set the session,
you redirect, and the session is gone.
Bind the Supabase client to the response object instead:
let response = NextResponse.redirect(`${origin}${target}`);
const supabase = createServerClient(url, key, {
cookies: {
getAll: () => request.cookies.getAll(),
setAll: (list) => {
list.forEach(({ name, value, options }) =>
response.cookies.set(name, value, options));
},
},
});
await supabase.auth.verifyOtp({ type, token_hash: tokenHash });
return response;That is what src/app/auth/callback/route.ts does, and the comment above it
says so, because this is exactly the code somebody simplifies six months later.
Profiles
A user row in auth.users is not yours to extend. The schema creates a matching
public.profiles row from a trigger:
create trigger on_auth_user_created
after insert on auth.users
for each row execute function public.handle_new_user();A trigger, not an insert in the sign-up handler, because a user can arrive
through OAuth, through an invite, or through the dashboard, and only the
database sees all three. Do it in application code and you will eventually have
a user with no profile and a page that crashes on profile.full_name.
Adding a provider
Enable it in the Supabase dashboard, add the redirect URL, and add a button. The client call is one line:
await supabaseBrowser().auth.signInWithOAuth({
provider: "github",
options: { redirectTo: `${location.origin}/auth/callback?next=/app` },
});No new route. The same callback handles it, since it already reads ?code= as
well as ?token_hash=.
Something wrong or missing on this page? Tell us.