Rentwise has three user roles — Owner, Tenant, Admin. Each role sees a completely different dashboard. Getting this wrong does not just break the UI — it potentially exposes one tenant's data to another. So I took auth seriously.
I started with the obvious approach: Next.js Middleware. Protect the /dashboard routes, redirect unauthenticated users to /login. Done, right?
Wrong. Here is what middleware alone misses — and how I ended up with three layers.
Next.js Middleware runs at the edge before the request reaches your page. It is fast, it redirects, and it handles the obvious case: 'is this user logged in at all?'
But here is the problem: Middleware does not protect React Server Components. A Server Component that fetches data does not go through Middleware. If your RSC fetches getPropertiesForOwner(userId) without validating the session server-side, an attacker who bypasses the redirect still gets the data.
Every dashboard layout re-validates the session using Better Auth's auth.api.getSession() on the server. If there is no valid session, it throws — which Next.js catches and redirects to the login page.
This catches the RSC data-fetch gap that Middleware misses. It is slightly slower (one extra DB read per navigation) but the security guarantee is worth it.
The third layer is a client-side SessionProvider that passes the validated session to all child components. Role-specific UI (admin-only buttons, owner-only property creation) is gated behind role checks using this context.
During testing, I discovered that Middleware was blocking unauthenticated tenant invite acceptance routes — new tenants could not even reach the sign-up page to accept their invite. The fix was redesigning the middleware matcher to allow public invite paths while still guarding all dashboard routes.
Without Layer 2, this would have been a silent security hole. Because Layer 2 caught it independently, I knew immediately when the fix was incomplete.
Full-Stack React Developer building SaaS and usuable apps.