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

  1. 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)
  2. Set Site URL and Redirect URLs in Supabase Auth settings for localhost and production.
  3. Limit the middleware matcher to protected paths only.
  4. Test the subscribe flow end-to-end with variant_id and redirect_to.