Skip to main content

Auth Routes

Sign in, sign up, and OAuth callback — the surface area you'll actually call.

Why this matters

Auth is the only piece of your directory that, when broken, takes the whole site down. Submissions silently fail, sponsorships can't activate, the admin panel becomes unreachable. The good news is that DirectoryLaunch ships with a Supabase-backed auth surface that's almost entirely client-side — there are exactly one route you call directly, one callback Supabase calls for you, and one middleware refresher that runs invisibly. Everything else is the Supabase SDK.

This page documents the three surfaces and gives you a working sign-in flow you can paste into a custom page in two minutes. If you only read one section, read Common pitfalls — every issue we've seen in the wild traces back to one of those four mistakes.

Most authentication happens client-side through the Supabase JS SDK — there's little to call directly. The exception is the OAuth callback.

Client-side sign in / sign up

Use the Supabase client, don't write your own API route:

import { getSupabaseClient } from '@/lib/supabase/client';
 
const supabase = getSupabaseClient();
 
// Email + password sign up
await supabase.auth.signUp({ email, password });
 
// Email + password sign in
await supabase.auth.signInWithPassword({ email, password });
 
// Google OAuth — redirects away
await supabase.auth.signInWithOAuth({
  provider: 'google',
  options: { redirectTo: `${window.location.origin}/auth/callback` },
});
 
// Sign out
await supabase.auth.signOut();
 
// Password reset email
await supabase.auth.resetPasswordForEmail(email, {
  redirectTo: `${window.location.origin}/auth/reset`,
});

GET /auth/callback

Handles the OAuth redirect after a user signs in with Google (or any other OAuth provider). Supabase sends back an ?code=... param which this route exchanges for a session cookie.

  • File: app/auth/callback/route.ts
  • Excluded from middleware (so the code exchange isn't interrupted)
  • Returns: redirect to /dashboard (or the next param if provided)

You don't call this route directly — Supabase redirects here automatically.

Session refresh

middleware.ts refreshes the auth cookie on every request. You don't have to do anything.

A real-world walkthrough: shipping a custom sign-in page

Suppose you want a marketing-flavoured sign-in page at /login instead of a generic auth modal. The whole thing is one client component:

app/login/page.tsx
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { getSupabaseClient } from '@/lib/supabase/client';
 
export default function LoginPage() {
  const router = useRouter();
  const supabase = getSupabaseClient();
  const [error, setError] = useState<string | null>(null);
 
  async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    const form = new FormData(e.currentTarget);
    const { error } = await supabase.auth.signInWithPassword({
      email: form.get('email') as string,
      password: form.get('password') as string,
    });
    if (error) return setError(error.message);
    router.push('/dashboard');
  }
 
  return (
    <form onSubmit={onSubmit}>
      <input name="email" type="email" required />
      <input name="password" type="password" required />
      {error && <p>{error}</p>}
      <button type="submit">Sign in</button>
    </form>
  );
}

After signInWithPassword succeeds, the middleware running on every request refreshes the cookie automatically — no extra client work. For Google OAuth, swap the form for a single signInWithOAuth({ provider: 'google' }) button; Supabase redirects to Google and back to /auth/callback, which lives at app/auth/callback/route.ts and exchanges the code for a session cookie before redirecting on to /dashboard.

Common pitfalls

  • Building your own /api/login route. Don't. Supabase already provides session-aware tokens — wrapping it in a custom API route adds a hop, breaks SSR-rendered auth state, and inevitably leaks the service role key. Always call the Supabase client directly from the browser.
  • Forgetting to exclude /auth/callback from the proxy/middleware. If middleware runs on the callback route, it tries to refresh a cookie that doesn't exist yet and the OAuth code exchange fails silently. Check middleware.ts/auth/callback should be in the matcher's negative-lookahead list.
  • Reading user state in Server Components without cookies()-aware client. Use createServerClient() from @/lib/supabase/server in Server Components and Route Handlers. The client-side singleton from @/lib/supabase/client won't see the cookie on the server.
  • Setting redirectTo to a literal URL in production. Always interpolate window.location.origin so previews on Vercel work. Hardcoded production URLs break OAuth flows on every preview deploy.

FAQ

Can I disable email/password sign-up entirely and only allow Google OAuth?

Yes — toggle email auth off in your Supabase project settings (Authentication → Providers → Email). The client SDK calls will return an error which you handle in your UI. Don't try to enforce this in your Next.js code; do it at the Supabase project level so the rule holds even for direct API access.

How do I get the current user in a Route Handler?

Use the server client and read auth.getUser():

import { createServerClient } from '@/lib/supabase/server';
const supabase = await createServerClient();
const { data: { user } } = await supabase.auth.getUser();

If user is null, return a 401. The lib/api/middleware.ts helper wraps this for you — most route handlers should call that instead of re-implementing the check.

Why does session expire after 1 hour in development but not in production?

Default Supabase JWT expiration is 60 minutes. The middleware refreshes silently on each request, so users don't notice — unless you keep a tab idle for over an hour. In production this is fine; if it's annoying in dev, increase the JWT expiry in your Supabase project settings.

See also