All playbooksNext.js + Supabase

Add a protected page

Add a page for signed-in users or admins only, with the check on the server and the data behind RLS.

When to use it

A new page that only signed-in users, or only admins, may open: an account area, a dashboard, settings.

Rules to keep

  • src/proxy.ts refreshes the session and redirects to /login to make navigation smoother. It is not the access control: the page checks the user itself, and the data is protected by RLS.
  • Identity and role are resolved on the server. The role comes from profiles (member or admin); never read it from a query string, a cookie or a form field.
  • Pages that read user data are Server Components. Use a Client Component only for the interactive parts and pass the data to it as props.

Steps

  1. Create src/app/<route>/page.tsx as an async Server Component and start it like src/app/page.tsx:

    import { redirect } from "next/navigation";
    import { createClient } from "@/lib/supabase/server";
    
    export default async function AccountPage() {
      const supabase = await createClient();
      const { data: { user } } = await supabase.auth.getUser();
      if (!user) redirect("/login");
      // ...
    }
    

    Use getUser(), which validates the session with Supabase, not getSession(), which only reads the cookie.

  2. For an admin page, read the role after the user check and stop with notFound(), so the page does not reveal that it exists:

    const { data: profile } = await supabase
      .from("profiles")
      .select("role")
      .eq("id", user.id)
      .single();
    if (profile?.role !== "admin") notFound();
    
  3. Check the proxy. In this foundation updateSession (src/lib/supabase/middleware.ts) sends every path except /login, /register and /auth to /login when there is no session, so a new page is already covered. If the file defines a list of protected prefixes, as the products built on this foundation do, add the new prefix to it.

  4. Load data with the same supabase client: queries run as the user and RLS applies. If the page needs a new table, follow the playbook "Add a table with RLS" first.

  5. Mutations go through a Server Action or a Route Handler that repeats the getUser() check, and the role check for admin actions: they can be called without opening the page.

  6. Link the page from the navigation only for users who can open it. If it opens a new area of the product, describe it in ARCHITECTURE.md.

Done when

  • the verification commands pass;
  • without a session the page redirects to /login;
  • for an admin page, a member account gets a 404;
  • every Server Action or Route Handler used by the page checks the user again.