AuthGate
The AuthGate component - route protection requiring authentication, and optionally an entitlement.
AuthGate (from @rising-tide/ui/auth) renders its children only to an authenticated viewer, and sends everyone else to a login path. It is the per-route or per-layout gate an app mounts itself. It is not the entry curtain - that is rendered for every app by RisingTideProviders and answers a different question (does this viewer hold a grant for this app).
Basic usage
import { AuthGate } from "@rising-tide/ui/auth";
export default function ProtectedPage() {
return (
<AuthGate>
<PageContent />
</AuthGate>
);
}An unauthenticated visitor is sent to loginPath with router.replace (default /). There is no return path: after signing in they land wherever the app's signupRedirect or its own sign-in handling puts them, not back on the page they were refused from. If you need that, carry the original path yourself.
The gate waits before deciding. While the stored auth method is being read back and the session restored (authResolved is false), and while the protocol user is loading, it renders fallback and redirects nobody - a cold load is "not yet", not "no".
Props
| Prop | Type | Purpose |
|---|---|---|
allowlist | AuthGateAllowEntry[] | Routes that bypass the gate. Each entry is { exact: "/" } or { prefix: "/claim/" } |
customMatcher | (pathname: string) => boolean | Runs before allowlist, for shapes a static list cannot express. Returning true allows the route; false defers to the list |
requiredEntitlement | string | Also require this entitlement of an authenticated viewer |
loginPath | string | Where unauthenticated (or unentitled) visitors are sent. Default / |
fallback | ReactNode | Rendered while auth state resolves and while redirecting. Default is a spinner |
Allowlist
Entries are objects, not path strings, and there is no pattern syntax:
<AuthGate
allowlist={[{ exact: "/" }, { prefix: "/claim/" }]}
loginPath="/auth"
>
{children}
</AuthGate>For a shape the list cannot express, pass customMatcher. rt-profile treats any single-segment path as a public profile lookup unless the segment is one of its own routes, and expresses that as a function rather than a list:
<AuthGate
allowlist={[{ exact: "/" }, { exact: "/achievements" }, { prefix: "/claim/" }]}
customMatcher={isPublicProfileRoute}
loginPath="/"
>
{children}
</AuthGate>Entitlement gate
You can require a specific entitlement, not just authentication:
<AuthGate requiredEntitlement="VIEW_ADMIN_PANEL">
<AdminPanel />
</AuthGate>An authenticated viewer who lacks the entitlement is treated exactly like an unauthenticated one: sent to loginPath. There is no 403 page. Render your own locked state instead if the viewer should learn why - useProtocolUser().hasEntitlement(name) is the same check.
In app layouts
Place AuthGate in a client component rendered from your root layout to protect all routes. A root layout is a Server Component and cannot pass a customMatcher function across the boundary, which is why rt-profile wraps the gate in a small client component of its own.
"use client";
import { AuthGate } from "@rising-tide/ui/auth";
export function AppAuthGate({ children }: { children: React.ReactNode }) {
return (
<AuthGate allowlist={[{ exact: "/" }, { prefix: "/claim/" }]}>
{children}
</AuthGate>
);
}