OWolf

AboutBlogProjects
©2025 OWolf.com

Privacy

Contact

Building A Complete Authentication System With Next.js 15 Supabase as a PWA

October 12, 2025

O. Wolfson

How to implement production-ready auth in a Progressive Web App with Next.js 15 App Router

When I set out to build a reference implementation for Supabase authentication in Next.js 15, I knew the challenges that lay ahead. Having struggled with auth setup in previous projects, I wanted to create a step-by-step guide that any developer could follow—complete with test pages, comprehensive documentation, and real-world examples.

The result? A fully functional PWA with enterprise-grade authentication, meticulously documented across 7 phases.

🔗 Live Demo: https://simple-pwa-eta.vercel.app/
💻 Source Code: https://github.com/owolfdev/simple-pwa


The Challenge

Implementing authentication in modern web apps involves juggling multiple concerns:

  • Server-side rendering (SSR) with Next.js App Router
  • Client-side state management for interactive components
  • Session persistence across page navigations
  • Email confirmation flows with proper redirects
  • Production deployment with environment-specific configs
  • Progressive Web App capabilities with offline support

Getting all these pieces to work together—especially in Next.js 15's new App Router paradigm—can be tricky. Most tutorials cover the basics but leave you hanging when it comes to production deployment or edge cases.


What I Built

A complete reference implementation featuring:

🔐 Full Authentication System

  • Email/password signup with confirmation
  • Secure login/logout
  • Session management via middleware
  • Protected routes and pages
  • Feature gating based on auth status

📱 Progressive Web App

  • Installable on any device
  • Offline capability with service workers
  • Mobile-responsive design
  • App-like experience in standalone mode

📚 Complete Documentation

  • 7-phase implementation guide
  • Test page for each phase
  • Code examples and patterns
  • Troubleshooting guide
  • Production deployment checklist

🧪 Testing Infrastructure

  • Verification page for each implementation phase
  • Integration test checklist
  • Multiple protected page patterns
  • Real-world examples

The 7-Phase Approach

Rather than dumping all the code at once and hoping it works, I broke the implementation into discrete, testable phases:

Phase 1: Setup & Dependencies

Install Supabase packages, configure environment variables, set up TypeScript paths. Each step is verified with a test page that shows exactly what's configured.

Test Page: /test-setup shows green checkmarks for each requirement.

Phase 2: Supabase Clients

Create three distinct Supabase clients:

  • Server client for Server Components and Route Handlers
  • Browser client for Client Components
  • Middleware client for session refresh

Test Page: /test-supabase verifies each client can connect to Supabase.

Phase 3: Middleware

Implement the critical middleware that refreshes auth sessions on every request. This is what keeps users logged in as they navigate your app.

typescript
export async function middleware(request: NextRequest) {
  return await updateSession(request);
}

Test Page: /test-middleware confirms middleware runs and sessions refresh.

Phase 4: Auth Routes

Create the route structure:

  • /auth/login - Login page
  • /auth/sign-up - Registration page
  • /auth/confirm - Email confirmation handler
  • /auth/error - Error display
  • /auth/sign-up-success - Post-signup messaging

Test Page: /test-routes provides one-click testing of each route.

Phase 5: Auth Components & Forms

Build the actual forms with full functionality:

  • LoginForm - Handles email/password authentication
  • SignUpForm - User registration with validation
  • LogoutButton - Secure logout
  • AuthButton - Shows auth status in header

Test Page: /test-auth guides you through the complete signup → confirm → login flow.

Phase 6: Supabase Dashboard Configuration

Configure production settings in your Supabase dashboard:

  • Add redirect URLs with wildcards (/**)
  • Set production site URL
  • Optionally customize email templates

Test Page: /test-config provides step-by-step dashboard configuration instructions.

Phase 7: Protected Pages & Integration

Implement real-world patterns:

  • Protected pages with automatic redirect
  • Conditional rendering based on auth status
  • Feature gating for members-only content
  • End-to-end integration testing

Test Page: /test-integration with complete E2E checklist.


Key Technical Decisions

Server vs. Client Components

Next.js 15's App Router requires careful consideration of where auth checks happen:

Server Components (default):

typescript
import { createClient } from "@/lib/supabase/server";

export default async function Page() {
  const supabase = await createClient();
  const { data } = await supabase.auth.getUser();

  if (!data.user) {
    redirect("/auth/login");
  }

  return <div>Protected content</div>;
}

Client Components (for interactive features):

typescript
"use client";
import { createClient } from "@/lib/supabase/client";

export function MyComponent() {
  const [user, setUser] = useState(null);

  useEffect(() => {
    const supabase = createClient();
    supabase.auth.getUser().then(({ data }) => {
      setUser(data.user);
    });
  }, []);

  return user ? <div>Logged in!</div> : <div>Not logged in</div>;
}

Session Management via Middleware

The middleware is crucial for maintaining auth state:

typescript
export async function middleware(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) {
          /* ... */
        },
      },
    }
  );

  // CRITICAL: This refreshes the session
  await supabase.auth.getClaims();

  return supabaseResponse;
}

This runs on every request, keeping sessions alive and preventing random logouts.

PWA Configuration

Balancing PWA features with development experience:

typescript
export default withPWA({
  dest: "public",
  register: true,
  disable: process.env.NODE_ENV === "development",
})(nextConfig);

PWA is disabled in development to avoid service worker caching issues during development, but fully enabled in production builds.


Production Deployment Gotchas

1. Redirect URL Wildcards

The most common mistake: forgetting the /** wildcard in Supabase redirect URLs.

❌ Wrong:

https://your-app.vercel.app/

✅ Correct:

https://your-app.vercel.app/**

Without the wildcard, email confirmation links (which go to /auth/confirm) will fail.

2. Use Domain URL, Not Deployment URL

Vercel gives you two URLs:

  • Deployment URL: app-abc123-username.vercel.app (changes per deployment)
  • Domain URL: your-app.vercel.app (permanent)

Always use the permanent domain URL in your Supabase config.

3. Environment Variables

Make sure your Vercel deployment has the same environment variables as local development:

  • NEXT_PUBLIC_SUPABASE_URL
  • NEXT_PUBLIC_SUPABASE_ANON_KEY

These must be set in Vercel's project settings.


Real-World Patterns

Pattern 1: Redirect on Unauthorized Access

Best for pages that absolutely require authentication:

typescript
export default async function ProtectedPage() {
  const supabase = await createClient();
  const { data } = await supabase.auth.getUser();

  if (!data.user) {
    redirect("/auth/login");
  }

  return <div>Secure content</div>;
}

Pattern 2: Conditional Rendering

Best for pages where you want to show a preview or teaser:

typescript
export default async function DashboardPage() {
  const supabase = await createClient();
  const { data } = await supabase.auth.getUser();

  return data.user ? (
    <div>Full dashboard with data</div>
  ) : (
    <div>
      <p>Sign up to access your dashboard</p>
      <Link href="/auth/sign-up">Sign up</Link>
    </div>
  );
}

Pattern 3: Feature Gating

Show different content based on auth status:

typescript
{
  user ? (
    <div>
      <h2>Welcome back, {user.email}!</h2>
      <Link href="/dashboard">Go to Dashboard</Link>
    </div>
  ) : (
    <div>
      <h2>Get started today</h2>
      <Link href="/auth/sign-up">Sign Up Free</Link>
    </div>
  );
}

Testing Strategy

Each phase includes a dedicated test page that verifies:

  1. Setup Test (/test-setup) - Dependencies and environment
  2. Supabase Test (/test-supabase) - Client connections
  3. Middleware Test (/test-middleware) - Session refresh
  4. Routes Test (/test-routes) - Page rendering
  5. Auth Test (/test-auth) - Login/logout functionality
  6. Config Guide (/test-config) - Dashboard setup
  7. Integration Test (/test-integration) - End-to-end flow

This test-driven approach ensures you catch issues early, before they compound into mysterious bugs.


Lessons Learned

1. Service Worker Caching Can Cause Infinite Loops

During development, the PWA service worker was causing constant page refreshes due to a transpilation error (_async_to_generator is not defined).

Solution: Disable PWA in development mode:

typescript
disable: process.env.NODE_ENV === "development";

2. Middleware Matcher Configuration Matters

The middleware must exclude static files to avoid unnecessary processing:

typescript
export const config = {
  matcher: [
    "/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
  ],
};

3. The themeColor Moved in Next.js 15

In Next.js 15, themeColor must be in a separate viewport export:

typescript
export const viewport: Viewport = {
  themeColor: "#ffffff",
};

Not in the metadata export like in previous versions.


Tech Stack

  • Next.js 15.5.4 - Latest App Router with React 19
  • Supabase - Backend-as-a-Service for auth
  • @supabase/ssr - SSR support for Next.js
  • TypeScript - Type safety throughout
  • Tailwind CSS v4 - Styling with the latest version
  • @ducanh2912/next-pwa - PWA support

What Makes This Different

Most auth tutorials give you the happy path. This implementation includes:

✅ Complete error handling - Every error state is handled
✅ Production deployment - Not just "works on localhost"
✅ Mobile responsive - Tested on all screen sizes
✅ PWA support - Installable and offline-capable
✅ Test infrastructure - Verify each step works
✅ Real-world patterns - Multiple auth patterns shown
✅ Comprehensive docs - Everything is documented


Get Started

Try the Live Demo

Visit https://simple-pwa-eta.vercel.app/ to:

  • Sign up for an account
  • Test the auth flow
  • Try protected pages
  • View the documentation
  • See test pages for each phase

Clone and Deploy

bash
git clone https://github.com/owolfdev/simple-pwa.git
cd simple-pwa
npm install

# Add your Supabase credentials to .env.local
# NEXT_PUBLIC_SUPABASE_URL=your-url
# NEXT_PUBLIC_SUPABASE_ANON_KEY=your-key

npm run dev

Visit http://localhost:3000/docs for the complete implementation guide.

Use as a Template

The entire codebase is structured as a reference implementation. You can:

  • Copy individual components for your project
  • Follow the 7-phase guide step-by-step
  • Use test pages to verify your implementation
  • Reference code patterns for auth checks

What's Next?

Potential additions to this reference implementation:

  • Password reset flow - Forgot password functionality
  • OAuth providers - Google, GitHub, etc.
  • Role-based access control - Admin vs. user permissions
  • Profile editing - Update user information
  • Database integration - Storing user data in Supabase
  • Subscription tiers - Paid vs. free features

All of these build on the foundation established in these 7 phases.


Conclusion

Building authentication doesn't have to be a mysterious black box. By breaking it into discrete phases, testing each step, and documenting thoroughly, we created a production-ready auth system that works reliably across development and production environments.

Whether you're building your first authenticated app or your tenth, having a reference implementation you can trust is invaluable. This project serves that purpose—a complete, tested, documented example of modern authentication in Next.js 15.

Resources

  • 🌐 Live Demo: https://simple-pwa-eta.vercel.app/
  • 💻 GitHub Repository: https://github.com/owolfdev/simple-pwa
  • 📚 Documentation: https://simple-pwa-eta.vercel.app/docs
  • 🧪 Test Pages: Available at /test-setup, /test-supabase, etc.

About the Tech Stack

Next.js 15.5.4 - The latest version with App Router and React 19
Supabase - Open-source Firebase alternative with robust auth
TypeScript - Type safety for fewer runtime errors
Tailwind CSS v4 - Utility-first styling with the latest features
PWA Support - Installable, offline-capable web app


Built as an educational resource for developers implementing Supabase authentication in Next.js 15 Progressive Web Apps. All code is production-tested and ready to use as a template.


Questions or suggestions? Check out the GitHub repository or test the live implementation.


Quick Start Checklist

If you're using this as a template, here's your quick start:

  • Clone the repository
  • npm install
  • Create .env.local with your Supabase credentials
  • npm run dev
  • Visit /test-setup to verify configuration
  • Follow phases 1-7 in /docs
  • Configure Supabase redirect URLs for production
  • Deploy to Vercel
  • Test production auth flow

That's it! You'll have working authentication in under an hour.


Happy coding! 🚀