Skip to main content

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

  1. Go to Authentication → Providers
  2. Enable Google provider
  3. Add OAuth credentials from Google Cloud Console
  4. 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:
RoleAccess
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.