Proxy and Protected Routes
In Next.js 16, proxy.ts replaces middleware.ts. It runs on every request before your routes render, making it perfect for session refresh and route protection. Without it, users would be logged out when their session token expires.
Outcome
Create a proxy that refreshes auth sessions and protects the /protected route from unauthenticated access.
Fast Track
- Implement
utils/supabase/proxy.tswithupdateSessionfunction - The root
proxy.tsis already configured to callupdateSession - The
app/protected/page.tsxis ready to display user info once auth works
Hands-on Exercise 1.5
Implement route protection with Next.js 16 proxy (TODO stub is provided):
Requirements:
- Implement
updateSessioninutils/supabase/proxy.tsto refresh auth cookies - The root
proxy.tsis already set up to call your function - Redirect unauthenticated users from
/protected/*to/sign-in - Redirect authenticated users from
/to/protected - Verify the protected page displays user information
Implementation hints:
- The proxy creates its own Supabase client with request/response cookie handling
- Always call
supabase.auth.getUser()to refresh the session - Return the modified response to ensure cookies are set correctly
- Use a matcher config to skip static files
Try It
-
Test unauthenticated access:
- Clear your cookies or use incognito
- Visit http://localhost:3000/protected
- You should be redirected to
/sign-in
-
Test authenticated redirect:
- Sign in at
/sign-in - Visit http://localhost:3000 (root)
- You should be redirected to
/protected
- Sign in at
-
Test protected page:
- While signed in, visit
/protected - You should see your email and user ID
- You should see a Sign Out button
- While signed in, visit
-
Test sign out:
- Click Sign Out
- You should be redirected to
/sign-in - Visiting
/protectedshould redirect to/sign-in
Commit
git add -A
git commit -m "feat(auth): add proxy and protected routes"Done-When
utils/supabase/proxy.tsexportsupdateSessionfunctionproxy.tsexists at project root- Unauthenticated users redirected from
/protected/*to/sign-in - Authenticated users redirected from
/to/protected - Protected page displays user email and ID
- Session persists across page refreshes
- Sign out works and redirects to
/sign-in
Solution
Step 1: Implement Session Update Utility
Replace the TODO stub in utils/supabase/proxy.ts:
import { createServerClient } from "@supabase/ssr";
import { NextResponse, type NextRequest } from "next/server";
export async function updateSession(request: NextRequest) {
let supabaseResponse = NextResponse.next({
request,
});
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll();
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value }) =>
request.cookies.set(name, value)
);
supabaseResponse = NextResponse.next({
request,
});
cookiesToSet.forEach(({ name, value, options }) =>
supabaseResponse.cookies.set(name, value, options)
);
},
},
}
);
// IMPORTANT: Avoid writing any logic between createServerClient and
// supabase.auth.getUser(). A simple mistake could make it very hard to debug
// issues with users being randomly logged out.
const user = await supabase.auth.getUser();
if (request.nextUrl.pathname.startsWith("/protected") && user.error) {
return NextResponse.redirect(new URL("/sign-in", request.url));
}
if (request.nextUrl.pathname === "/" && !user.error) {
return NextResponse.redirect(new URL("/protected", request.url));
}
// IMPORTANT: You *must* return the supabaseResponse object as it is. If you're
// creating a new response object with NextResponse.next() make sure to:
// 1. Pass the request in it, like so:
// const myNewResponse = NextResponse.next({ request })
// 2. Copy over the cookies, like so:
// myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())
// 3. Change the myNewResponse object to fit your needs, but avoid changing
// the cookies!
// 4. Finally:
// return myNewResponse
// If this is not done, you may be causing the browser and server to go out
// of sync and terminate the user's session prematurely!
return supabaseResponse;
}Step 2: Verify Root Proxy
The root proxy.ts is already configured in the starter:
import { type NextRequest } from "next/server";
import { updateSession } from "@/utils/supabase/proxy";
export async function proxy(request: NextRequest) {
return await updateSession(request);
}
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* Feel free to modify this pattern to include more paths.
*/
"/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
],
};This file calls your updateSession function on every request.
Step 3: Verify Protected Layout
The protected layout is already in the starter at app/protected/layout.tsx:
import Content from "@/components/content";
export default function ProtectedLayout({
children,
}: {
children: React.ReactNode;
}) {
return <Content>{children}</Content>;
}Step 4: Verify Protected Page
The protected page is already in the starter at app/protected/page.tsx:
import { createSupabaseClient } from "@/utils/supabase/server";
import AuthPageSignOutButton from "@/components/auth-sign-out-button";
export default async function ProtectedPage() {
const supabase = await createSupabaseClient();
const {
data: { user },
} = await supabase.auth.getUser();
return (
<div className="space-y-8">
<div className="flex items-center justify-between">
<div className="flex flex-col">
<h1 className="text-2xl font-medium">My Guild Profile</h1>
<p className="text-muted-foreground mt-2">
Manage your guild membership
</p>
</div>
<AuthPageSignOutButton />
</div>
<div className="space-y-6">
<div className="border rounded-lg p-6 space-y-4">
<h2 className="font-medium">Guild Member Information</h2>
<div className="grid gap-2 text-sm">
<div className="grid grid-cols-[120px_1fr]">
<div className="text-muted-foreground">Email</div>
<div>{user?.email}</div>
</div>
<div className="grid grid-cols-[120px_1fr]">
<div className="text-muted-foreground">Member ID</div>
<div className="font-mono text-xs">{user?.id}</div>
</div>
</div>
</div>
</div>
</div>
);
}The AuthPageSignOutButton is a pre-built client component in the starter that handles sign-out with a loading spinner.
File Structure After This Lesson
subscription-storefront/
├── proxy.ts ← Calls your updateSession function
├── app/
│ ├── (auth)/
│ │ ├── layout.tsx
│ │ ├── sign-up/page.tsx
│ │ └── sign-in/page.tsx
│ ├── protected/
│ │ ├── layout.tsx
│ │ └── page.tsx ← Displays user info
│ └── actions.ts ← Implemented in lesson 1.4
└── utils/
├── redirect.ts
└── supabase/
├── client.ts ← Implemented in lesson 1.3
├── server.ts ← Implemented in lesson 1.3
└── proxy.ts ← Implemented in this lesson
How the Proxy Works
Browser Request → proxy.ts → updateSession()
↓
Create Supabase client
(reads cookies from request)
↓
Call auth.getUser()
(refreshes session if needed)
↓
Check protection rules
↓ ↓
/protected? Authenticated?
+ no user + at root?
↓ ↓
Redirect to Redirect to
/sign-in /protected
↓
Return response
(with updated cookies)
Why proxy.ts Instead of middleware.ts?
Next.js 16 renamed middleware to proxy to clarify its purpose. The proxy runs in front of your app on every request, making it ideal for:
- Session refresh (what we're doing)
- Redirects based on auth state
- A/B testing
- Geolocation-based routing
Troubleshooting
"Users randomly logged out":
Make sure you return the supabaseResponse object, not a new NextResponse.next(). The response contains updated cookies that must be sent to the browser.
Redirect loops:
Check your conditions in updateSession. Make sure you're not redirecting authenticated users away from pages they should access.
Proxy not running:
- Verify
proxy.tsis at the project root (same level aspackage.json) - Check the matcher config includes your routes
- Restart the dev server
Session not persisting:
- Check browser cookies for
sb-prefixed cookies - Verify environment variables are correct
- Make sure
auth.getUser()is called in the proxy
Section Complete
You now have a working authentication system:
- Lesson 1.1: Deployed starter repo
- Lesson 1.2: Configured Supabase credentials
- Lesson 1.3: Created browser and server clients
- Lesson 1.4: Built sign-up and sign-in pages
- Lesson 1.5: Protected routes with proxy
Next up in Section 2: You'll integrate Stripe to add subscription billing to your storefront.
Was this helpful?