Authentication
Garmint uses Supabase Auth with multiple sign-in methods.
Supported Methods
Google OAuth One-click sign in with Google account
Magic Link Passwordless email authentication
Email/Password Traditional credentials (optional)
Configuration
Supabase Dashboard
Go to Authentication → Providers
Enable Google provider
Add OAuth credentials from Google Cloud Console
Configure redirect URLs
Environment Variables
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
NEXT_PUBLIC_SITE_URL=http://localhost:3000
Redirect URLs
Add to Supabase Auth settings:
http://localhost:3000/api/auth/callback
https://yourapp.vercel.app/api/auth/callback
Login Page
The login page (/login) handles all auth flows:
// app/login/page.tsx
"use client" ;
import { createClient } from "@/lib/supabase/client" ;
export default function LoginPage () {
const supabase = createClient ();
const signInWithGoogle = async () => {
await supabase . auth . signInWithOAuth ({
provider: "google" ,
options: {
redirectTo: ` ${ window . location . origin } /api/auth/callback` ,
},
});
};
const signInWithEmail = async ( email : string ) => {
await supabase . auth . signInWithOtp ({
email ,
options: {
emailRedirectTo: ` ${ window . location . origin } /api/auth/callback` ,
},
});
};
return (
< div className = "flex min-h-screen items-center justify-center" >
< Button onClick = { signInWithGoogle } >
Continue with Google
</ Button >
{ /* Magic link form */ }
</ div >
);
}
Auth Callback
Handle OAuth redirects:
// app/api/auth/callback/route.ts
import { createClient } from "@/lib/supabase/server-client" ;
import { NextResponse } from "next/server" ;
export async function GET ( request : Request ) {
const requestUrl = new URL ( request . url );
const code = requestUrl . searchParams . get ( "code" );
const origin = requestUrl . origin ;
if ( code ) {
const supabase = await createClient ();
await supabase . auth . exchangeCodeForSession ( code );
}
return NextResponse . redirect ( ` ${ origin } /projects` );
}
Middleware Protection
Protected routes require authentication:
// middleware.ts
import { createServerClient } from "@supabase/ssr" ;
import { NextResponse } from "next/server" ;
import type { NextRequest } from "next/server" ;
const PROTECTED_ROUTES = [ "/settings" , "/lookbook" , "/orders" , "/collections" ];
const PUBLIC_ROUTES = [ "/" , "/login" , "/landing" , "/pricing" ];
export async function middleware ( request : NextRequest ) {
let response = NextResponse . next ();
const supabase = createServerClient (
process . env . NEXT_PUBLIC_SUPABASE_URL ! ,
process . env . NEXT_PUBLIC_SUPABASE_ANON_KEY ! ,
{
cookies: {
get : ( name ) => request . cookies . get ( name )?. value ,
set : ( name , value , options ) => {
response . cookies . set ({ name , value , ... options });
},
remove : ( name , options ) => {
response . cookies . set ({ name , value: "" , ... options });
},
},
}
);
const { data : { session } } = await supabase . auth . getSession ();
const isProtected = PROTECTED_ROUTES . some ( route =>
request . nextUrl . pathname . startsWith ( route )
);
if ( isProtected && ! session ) {
return NextResponse . redirect ( new URL ( "/login" , request . url ));
}
return response ;
}
Session Context
Server-side session access:
// lib/auth/session.ts
import { cache } from "react" ;
import { getSupabaseServerClient } from "@/lib/supabase/server-client" ;
export const getSessionContext = cache ( async ( projectId ?: string ) => {
const supabase = await getSupabaseServerClient ();
const { data : { session } } = await supabase . auth . getSession ();
if ( ! session ) {
throw new Error ( "No session available" );
}
const { data : profile } = await supabase
. from ( "profiles" )
. select ( "*" )
. eq ( "id" , session . user . id )
. single ();
// Fetch memberships, resolve project...
return {
supabase ,
session ,
profile ,
project ,
memberships ,
};
});
Profile Creation
Profiles auto-create on signup via database trigger:
create or replace function public .handle_new_user()
returns trigger
language plpgsql
security definer
as $$
begin
insert into public . profiles (id, email)
values ( new . id , new . email )
on conflict (id) do update
set email = excluded . email , updated_at = now ();
return new;
end ;
$$;
create trigger on_auth_user_created
after insert on auth . users
for each row execute procedure public . handle_new_user ();
Project Bootstrap
New users get a personal project automatically:
async function bootstrapPersonalProject ( supabase , profile ) {
const service = getSupabaseServiceRoleClient ();
const projectName = profile . display_name
? ` ${ profile . display_name } 's Workspace`
: "My Workspace" ;
const { data : project } = await service
. from ( "projects" )
. insert ({
name: projectName ,
slug: slugifyProjectName ( projectName ),
status: "active" ,
})
. select ()
. single ();
await service
. from ( "project_members" )
. insert ({
project_id: project . id ,
user_id: profile . id ,
role: "owner" ,
});
}
Roles & Permissions
User roles stored in profiles.role:
Role Access memberStandard user access adminFull system access operatorOps dashboard access
Check operator access:
// lib/ops/access.ts
export async function requireOperatorAccess () {
const { profile } = await getSessionContext ();
if ( profile . role !== "operator" && profile . role !== "admin" ) {
throw new Error ( "Operator access required" );
}
return profile ;
}
Sign Out
// Client-side
const supabase = createClient ();
await supabase . auth . signOut ();
router . push ( "/login" );
// Server action
"use server" ;
import { createClient } from "@/lib/supabase/server-client" ;
import { redirect } from "next/navigation" ;
export async function signOut () {
const supabase = await createClient ();
await supabase . auth . signOut ();
redirect ( "/login" );
}
Security Notes
Always use the server client for sensitive operations. The anon key is exposed to browsers.
Use getSessionContext() in Server Components and API routes for cached, type-safe session access.
The service role key bypasses RLS. Only use in server-side code for admin operations.