Rules Hub
Code Ruleset
Rule priority, scope & exceptions
Use this to align rules with the senior-level structure (P0/P1/P2, scope, exceptions/tradeoffs).
Re-check authorization inside Next.js server actions and RSC data loads
Authorize server actions and sensitive server-component data access at the server data boundary; do not rely solely on layouts or UI visibility.
Bad example
| 1 | // app/dashboard/layout.tsx — assumes this guards everything below |
| 2 | export default async function Layout({ children }) { |
| 3 | const session = await getSession(); |
| 4 | if (!session) redirect('/login'); |
| 5 | return <>{children}</>; |
| 6 | } |
| 7 |
|
| 8 | // app/dashboard/actions.ts — relies on the layout having run |
| 9 | 'use server'; |
| 10 | export async function updatePortfolioName(id: number, name: string) { |
| 11 | // No auth here. Server actions are POST endpoints callable directly; |
| 12 | // the layout does NOT run before an action invocation. |
| 13 | await db.portfolio.update({ where: { id }, data: { name } }); |
| 14 | } |
Explanation (EN)
A layout guard does not authorize an independently invoked mutation or protect a nested data read.
Objašnjenje (HR)
Zaštita layouta ne autorizira neovisno pozvanu mutaciju niti štiti ugniježđeno čitanje podataka.
Good example
| 1 | // app/dashboard/actions.ts |
| 2 | 'use server'; |
| 3 | export async function updatePortfolioName(id: number, name: string) { |
| 4 | const session = await getSession(); |
| 5 | if (!session) throw new Error('Unauthorized'); |
| 6 |
|
| 7 | const parsedName = z.string().min(1).max(120).parse(name); |
| 8 |
|
| 9 | // Owner-scoped update: a non-owner matches 0 rows. |
| 10 | const result = await db.portfolio.updateMany({ |
| 11 | where: { id, ownerId: session.userId }, |
| 12 | data: { name: parsedName }, |
| 13 | }); |
| 14 | if (result.count === 0) throw new Error('Not found'); |
| 15 | } |
Explanation (EN)
The action establishes the session and scopes the mutation by owner inside itself, so it is safe regardless of how it was invoked or whether any layout/middleware ran. Treat every server action and server-component data load as an independent, directly-reachable endpoint that must authorize itself.
Objašnjenje (HR)
Action uspostavlja sesiju i ograničava mutaciju po vlasniku unutar sebe, pa je siguran bez obzira na to kako je pozvan ili je li ijedan layout/middleware izvršen. Tretiraj svaki server action i dohvat podataka u server komponenti kao neovisan, izravno dostupan endpoint koji se mora sam autorizirati.
Notes (EN)
Treat server actions as callable mutation entry points and enforce identity plus object-level authorization at the server data-access boundary for both actions and server-rendered reads.
Exceptions / Tradeoffs (EN)
Next.js middleware is fine for coarse, defense-in-depth gating (redirect unauthenticated users, attach headers) but must never be the only authorization layer; matcher gaps and the fact that middleware can be bypassed for certain invocation paths make it insufficient on its own. Keep the authoritative check next to the data access. A server component is not itself necessarily a directly callable action endpoint. Centralized data-access helpers may provide the authoritative check; a layout or hidden button alone cannot provide it.
Iznimke / Tradeoffi (HR)
Next.js middleware može služiti kao dodatna gruba zaštita, ali zbog različitih putova pozivanja i matchera ne smije biti jedina autorizacija. Mjerodavnu provjeru drži uz pristup podacima, po potrebi u zajedničkom podatkovnom sloju. Server komponenta nije nužno izravno pozivljiv action endpoint; layout ili skriveni gumb sami ne štite podatke.