Authentication
Init provides a complete authentication system built on Supabase Auth. This guide covers the authentication architecture, implementation details, and how to customize it for your needs.
Overview
Supabase Auth handles all authentication concerns including:
- User registration and login
- Email verification
- Password reset flows
- Social OAuth providers
- Session management
- Multi-factor authentication (MFA)
- Team-based access control
Architecture
Authentication Flow
graph TD
A[User Login] --> B[Supabase Auth]
B --> C[JWT Token]
C --> D[tRPC Context]
D --> E[Protected Routes]
E --> F[Database Queries]
Key Components
- Supabase Clients - Different clients for server/browser/middleware
- tRPC Integration - Authentication context for API calls
- Middleware - Route protection and session management
- Auth Forms - Login, register, and password reset components
Supabase Client Configuration
Init uses multiple Supabase clients optimized for different environments:
Server Client
For server-side operations (API routes, middleware):
// packages/db/src/supabase-server-client.ts
import { cookies } from "next/headers";
import { createServerClient } from "@supabase/ssr";
export const getSupabaseServerClient = () => {
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll: async () => {
const cookieStore = await cookies();
return cookieStore.getAll();
},
setAll: async (cookiesToSet) => {
const cookieStore = await cookies();
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options),
);
},
},
},
);
};
Browser Client
For client-side operations:
// packages/db/src/supabase-browser-client.ts
import { createBrowserClient } from "@supabase/ssr";
export const getSupabaseBrowserClient = () => {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);
};
Middleware Client
For Next.js middleware:
// packages/db/src/supabase-middleware-client.ts
import { NextResponse } from "next/server";
import { createServerClient } from "@supabase/ssr";
export const getSupabaseMiddlewareClient = (request: NextRequest) => {
let response = NextResponse.next({
request: { headers: request.headers },
});
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) => {
request.cookies.set({ name, value, ...options });
response.cookies.set({ name, value, ...options });
},
remove: (name, options) => {
request.cookies.set({ name, value: "", ...options });
response.cookies.set({ name, value: "", ...options });
},
},
},
);
return { supabase, response };
};
tRPC Integration
Authentication is deeply integrated with tRPC for type-safe API access:
Context Setup
// packages/api/src/trpc.ts
export const createTRPCContext = async (opts: { headers: Headers }) => {
const supabase = getSupabaseServerClient();
// Handle both header tokens (mobile) and cookies (web)
const token = opts.headers.get("authorization");
const { data } = token
? await supabase.auth.getUser(token)
: await supabase.auth.getUser();
return {
headers: opts.headers,
user: data.user, // Current authenticated user
supabase, // Supabase client
db, // Database client
};
};
Protected Procedures
// packages/api/src/trpc.ts
export const protectedProcedure = t.procedure.use(
t.middleware(({ ctx, next }) => {
if (!ctx.user) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return next({
ctx: {
// Infers the `user` as non-nullable
user: ctx.user,
...ctx,
},
});
}),
);
Authentication Routes
Login
Email/password authentication:
// packages/api/src/auth/auth-router.ts
export const authRouter = createTRPCRouter({
signInWithPassword: publicProcedure
.input(signInWithPasswordInput)
.mutation(async ({ input, ctx }) => {
const { data, error } = await ctx.supabase.auth.signInWithPassword({
email: input.email,
password: input.password,
});
if (error) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: error.message,
});
}
return data;
}),
});
Registration
User registration with profile creation:
signUp: publicProcedure
.input(signUpInput)
.mutation(async ({ input, ctx }) => {
const { email, password, firstName, lastName } = input;
const { data, error } = await ctx.supabase.auth.signUp({
email,
password,
options: {
data: {
first_name: firstName,
last_name: lastName,
full_name: `${firstName} ${lastName}`,
},
},
});
if (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: error.message,
});
}
return data;
}),
OAuth Providers
Social authentication support:
signInWithOAuth: publicProcedure
.input(signInWithOAuthInput)
.mutation(async ({ input, ctx }) => {
const { provider, redirectTo } = input;
const { data, error } = await ctx.supabase.auth.signInWithOAuth({
provider,
options: {
redirectTo,
},
});
if (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: error.message,
});
}
return data;
}),
Frontend Components
Auth Form
Reusable authentication form component:
// apps/nextjs/src/app/(auth)/_components/auth-form.tsx
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { Button, Input, Label } from "@repo/ui";
import { api } from "@/trpc/react";
interface AuthFormProps {
type: "login" | "register";
}
export function AuthForm({ type }: AuthFormProps) {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const router = useRouter();
const signIn = api.auth.signInWithPassword.useMutation({
onSuccess: () => router.push("/dashboard"),
onError: (error) => toast.error(error.message),
});
const signUp = api.auth.signUp.useMutation({
onSuccess: () => router.push("/auth/verify"),
onError: (error) => toast.error(error.message),
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
try {
if (type === "login") {
await signIn.mutateAsync({ email, password });
} else {
await signUp.mutateAsync({ email, password, firstName, lastName });
}
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<div>
<Label htmlFor="password">Password</Label>
<Input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<Button type="submit" className="w-full" disabled={loading}>
{loading ? "Loading..." : type === "login" ? "Sign In" : "Sign Up"}
</Button>
</form>
);
}
Route Protection
Middleware Protection
Protect routes at the middleware level:
// apps/nextjs/src/middleware.ts
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { getSupabaseMiddlewareClient } from "@repo/db/supabase-middleware-client";
export async function middleware(request: NextRequest) {
const { supabase, response } = getSupabaseMiddlewareClient(request);
const {
data: { user },
} = await supabase.auth.getUser();
// Protected routes
const protectedPaths = ["/dashboard", "/account"];
const isProtectedPath = protectedPaths.some((path) =>
request.nextUrl.pathname.startsWith(path),
);
if (isProtectedPath && !user) {
const redirectUrl = new URL("/auth/login", request.url);
redirectUrl.searchParams.set("redirectedFrom", request.nextUrl.pathname);
return NextResponse.redirect(redirectUrl);
}
// Redirect authenticated users from auth pages
const authPaths = ["/auth/login", "/auth/register"];
const isAuthPath = authPaths.includes(request.nextUrl.pathname);
if (isAuthPath && user) {
return NextResponse.redirect(new URL("/dashboard", request.url));
}
return response;
}
export const config = {
matcher: [
"/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
],
};
Component-Level Protection
Protect components with auth checks:
// components/auth-guard.tsx
"use client";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
import { api } from "@/trpc/react";
interface AuthGuardProps {
children: React.ReactNode;
fallback?: React.ReactNode;
}
export function AuthGuard({ children, fallback }: AuthGuardProps) {
const router = useRouter();
const { data: workspace, isLoading } = api.auth.workspace.useQuery();
useEffect(() => {
if (!isLoading && !workspace?.user) {
router.push("/auth/login");
}
}, [workspace, isLoading, router]);
if (isLoading) {
return <div>Loading...</div>;
}
if (!workspace?.user) {
return fallback || null;
}
return <>{children}</>;
}
Team-Based Access Control
Team Membership
Init includes team-based access control:
// Database schema (packages/db/src/schema.ts)
export const teams = pgTable("teams", {
id: uuid("id").primaryKey().defaultRandom(),
name: text("name").notNull(),
slug: text("slug").notNull().unique(),
createdAt: timestamp("created_at").defaultNow(),
});
export const teamMembers = pgTable("team_members", {
id: uuid("id").primaryKey().defaultRandom(),
teamId: uuid("team_id")
.references(() => teams.id)
.notNull(),
userId: uuid("user_id").notNull(),
role: text("role", { enum: ["OWNER", "ADMIN", "MEMBER"] }).notNull(),
createdAt: timestamp("created_at").defaultNow(),
});
Team Context
Get user's team memberships:
// packages/api/src/auth/auth-router.ts
workspace: publicProcedure.query(async ({ ctx }) => {
const teamMembers = ctx.user
? await ctx.db.query.teamMembers.findMany({
where: eq(teamMembers.userId, ctx.user.id),
with: { team: true },
})
: [];
return {
user: ctx.user,
teams: teamMembers.map(tm => tm.team),
activeTeam: teamMembers[0]?.team, // Default to first team
};
}),
Password Reset Flow
Request Reset
requestPasswordReset: publicProcedure
.input(requestPasswordResetInput)
.mutation(async ({ input, ctx }) => {
const { data, error } = await ctx.supabase.auth.resetPasswordForEmail(
input.email,
{
redirectTo: `${input.redirectTo}/auth/password-update`,
}
);
if (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: error.message,
});
}
return data;
}),
Update Password
updatePassword: publicProcedure
.input(updatePasswordInput)
.mutation(async ({ input, ctx }) => {
const { data, error } = await ctx.supabase.auth.updateUser({
password: input.password,
});
if (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: error.message,
});
}
return data;
}),
Session Management
Client-Side Session
// hooks/use-auth.ts
"use client";
import { api } from "@/trpc/react";
export function useAuth() {
const { data: workspace, isLoading } = api.auth.workspace.useQuery();
return {
user: workspace?.user,
teams: workspace?.teams || [],
isLoading,
isAuthenticated: !!workspace?.user,
};
}
Server-Side Session
// In server components
import { getSupabaseServerClient } from "@repo/db/supabase-server-client";
export default async function DashboardPage() {
const supabase = getSupabaseServerClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
redirect("/auth/login");
}
return <div>Welcome {user.email}</div>;
}
Environment Variables
Required environment variables:
# Supabase Configuration
NEXT_PUBLIC_SUPABASE_URL=your-supabase-project-url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-supabase-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
# Database
DATABASE_URL=your-database-connection-string
Customization
Adding OAuth Providers
Configure providers in Supabase dashboard, then use:
const signInWithGoogle = api.auth.signInWithOAuth.useMutation();
const handleGoogleLogin = () => {
signInWithGoogle.mutate({
provider: "google",
redirectTo: window.location.origin + "/dashboard",
});
};
Custom User Metadata
Store additional user data:
// During registration
const { data, error } = await supabase.auth.signUp({
email,
password,
options: {
data: {
first_name: firstName,
last_name: lastName,
company: companyName,
role: userRole,
},
},
});
Multi-Factor Authentication
Enable MFA in Supabase dashboard:
// Enable MFA for user
const { data, error } = await supabase.auth.mfa.enroll({
factorType: "totp",
});
// Verify MFA challenge
const { data, error } = await supabase.auth.mfa.verify({
factorId: data.id,
challengeId: challenge.id,
code: userEnteredCode,
});
Security Best Practices
1. Environment Security
- Never commit secrets to version control
- Use different keys for development/production
- Rotate keys regularly
2. Session Security
- Configure appropriate session timeouts
- Use secure cookies in production
- Implement proper logout flows
3. Input Validation
- Validate all inputs with Zod schemas
- Sanitize user data
- Use parameterized queries
4. Route Protection
- Protect sensitive routes with middleware
- Implement proper role-based access
- Use server-side session validation
Next Steps
Now that you understand authentication:
- Database setup - Learn about database architecture
- Team management - Implement team-based features
- User profiles - Build user profile management
- Social auth - Add OAuth providers