Overview
Auth is handled by Supabase and enforced in Next.js via middleware. Public marketing/docs live in (app)
, while dashboard
, projects
, settings
, and playground
are protected. Auth pages are grouped under (auth-pages)
(sign-in, sign-up, reset-password).
Routes & protection
- Public: app/(app)/*
- Auth: app/(auth-pages)/sign-in, sign-up, reset-password
- Protected: app/dashboard/, app/projects/, app/settings/, app/playground/
- robots.ts disallows /auth*, /dashboard*, and /api/*
Protected routes
Middleware checks the session with createServerClient
. If no user is found, it redirects to sign-in and preserves intent parameters (variant_id
, redirect_to
).
// middleware.ts
import { NextResponse } from 'next/server'
import { createServerClient } from '@supabase/ssr'
export const config = {
matcher: ['/dashboard/:path*','/projects/:path*','/settings/:path*','/playground/:path*'],
}
export async function middleware(req: Request) {
const url = new URL(req.url)
const supabase = createServerClient({ cookies: { /* read/write from request/response */ } })
const { data: { user } } = await supabase.auth.getUser()
if (user) return NextResponse.next()
const signin = new URL('/auth/sign-in', url.origin)
// preserve purchase intent
signin.search = url.search // keeps ?variant_id=&redirect_to=
return NextResponse.redirect(signin)
}
Keep middleware matcher limited to protected paths to reduce overhead in dev.
Auth pages & layout
Sign-in, Sign-up, and Reset Password share a reusable AuthLayout
with consistent transitions. Errors are shown via ActionMessageToast
. Social buttons are rendered from a small map using an icon
button variant to keep styling consistent.
Social logins
Providers: Google, GitHub, Apple, Facebook.
- Configure OAuth apps in the Supabase dashboard.
- Add redirect URLs for both local and production.
- Expose only the anon key to the client; keep service role on the server.
Add provider callback URLs for each environment in Supabase → Authentication → Providers.
Subscribe flow
If an unauthenticated user clicks Subscribe, send them to sign-up with ?variant_id=<lemon-variant>&redirect_to=/checkout
. When toggling between Sign In and Sign Up, always carry these params so purchase intent survives. After successful auth, /auth/callback
reads the params and continues to Lemon Squeezy checkout.
// Example link that keeps current params
<Link href={{ pathname: '/auth/sign-up', query: Object.fromEntries(new URLSearchParams(search)) }}>Sign up</Link>
Always preserve redirect_to and variant_id when switching between auth screens.
Security notes
- Service role key is server-only (webhooks, admin actions).
- RLS ensures users can read/update only their own rows.
- Do not index auth pages;
robots.ts
already disallows /auth and /dashboard.
Never expose the service role key to the browser or client bundles.
Local development
- Add env to
.env.local
:NEXT_PUBLIC_SUPABASE_URL
NEXT_PUBLIC_SUPABASE_ANON_KEY
SUPABASE_SERVICE_ROLE_KEY
(server-only)NEXT_PUBLIC_APP_URL
(used by redirects and SEO)
- Set Site URL and Redirect URLs in Supabase Auth settings for localhost and production.
- Limit the middleware matcher to protected paths only.
- Test the subscribe flow end-to-end with
variant_id
andredirect_to
.