# Protecting your own routes (/docs/admin/auth/protecting-routes)

---
title: Protecting your own routes
description: Helpers that turn a Request into an admin check, a beforeLoad guard for TanStack Start, a middleware for Next, and the one case where a guard lets the request through.
kind: guide
package: "@questpie/admin"
---

The panel guards `/admin` on its own. A page of your own that reads the same
data has no guard at all. These helpers run the check the panel runs, against a
plain `Request`.

## The helpers

All of them come from `@questpie/admin/server`.

| Helper                      | Returns                                               |
| --------------------------- | ----------------------------------------------------- |
| `requireAdminAuth(opts)`    | A 302 `Response` to the login path, or `null`.        |
| `getAdminSession(opts)`     | The Better Auth session, or `null`.                   |
| `isAdminUser(opts)`         | `true` when the session user holds the required role. |
| `getNextAdminSession(opts)` | The same session, from a Next `Headers` object.       |

The first three take `{ request, app }`. `getNextAdminSession` takes
`{ headers, app }` and builds the request itself.

`requireAdminAuth` takes three more options, and `isAdminUser` takes the first
of them.

| Option          | Default          |
| --------------- | ---------------- |
| `requiredRole`  | `"admin"`        |
| `loginPath`     | `"/admin/login"` |
| `redirectParam` | `"redirect"`     |

```ts
import { requireAdminAuth } from "@questpie/admin/server";
import { app } from "#questpie";

export async function handler(request: Request) {
	const redirect = await requireAdminAuth({ request, app });
	if (redirect) return redirect;

	// past this line the session holds role "admin"
}
```

A `null` from `requireAdminAuth` means allowed. That reads backwards the first
time, so hold the return value and test it before you carry on.

## TanStack Start

`createTanStackAuthGuard` wraps `requireAdminAuth` and throws the redirect. That
is what TanStack Router expects from `beforeLoad`.

```ts title="src/routes/reports.tsx"
import { createFileRoute } from "@tanstack/react-router";
import { createTanStackAuthGuard } from "@questpie/admin/server";
import { app } from "#questpie";

export const Route = createFileRoute("/reports")({
	beforeLoad: createTanStackAuthGuard({ app }),
	component: Reports,
});
```

It reads the request off `context.request`. With no request there it logs a
warning and returns, letting the route render. So this guard needs SSR on.

Keep it off `/admin` itself. This guard has no public-path list, and a
`beforeLoad` on the parent route runs for every child. It would guard the login
and setup pages too.

`createTanStackSessionLoader({ app })` is the other half. Use it as `loader` and
the route data carries `{ session }`. With no request, or no auth configured, it
carries `{ session: null }`.

## Next

`createNextAuthMiddleware({ app })` takes the same three options plus two path
lists. `protectedPaths` defaults to `["/admin"]`. `publicPaths` defaults to the
login, forgot-password, reset-password and accept-invite paths under it. Both
lists match on prefix.

```ts title="middleware.ts"
import { createNextAuthMiddleware } from "@questpie/admin/server";
import { app } from "#questpie";

export default createNextAuthMiddleware({
	app,
	publicPaths: [
		"/admin/login",
		"/admin/forgot-password",
		"/admin/reset-password",
		"/admin/setup",
	],
});

export const config = { matcher: ["/admin/:path*"] };
```

`/admin/setup` is missing from the default list. Pass your own list, as above.
Otherwise the middleware bounces your first admin off the one page that would
create them.

A request that may continue gets an empty `200` carrying the
`x-middleware-next: 1` header. A request that may not gets the redirect.

In a server component or a route handler, reach for `getNextAdminSession`
instead. It builds the `Request` for you from `headers()`.

<Callout type="warn" title="No auth config means no guard">
	`requireAdminAuth` returns `null` when `app.auth` is missing. It logs a
	warning and lets the request through. An app with no auth configured is not a
	locked app. It is an open one.
</Callout>

## The redirect parameter is written, never read

Every guard appends `?redirect=<path>` to the login URL. The built-in login page
ignores it. After a successful sign-in it navigates to the panel's `basePath`.
That is `/admin`.

A 401 from a query does something else again. The client sends you to
`login?returnUrl=<path>`, and nothing reads that either.

So people land on the dashboard, not on the page they asked for. If that matters
to your project, read the parameter yourself and pass it to `<LoginPage>` as
`redirectTo`.

## Where each topic lives

| Topic                                    | Page                                          |
| ---------------------------------------- | --------------------------------------------- |
| The role rule these helpers check        | [Authentication](/docs/admin/auth)            |
| Rules on the data itself, not the route  | [Access control](/docs/schema/access-control) |
| Routes of your own, and their own access | [Routes](/docs/code/routes)                   |
