Permissions and access control
Learn how to use Permix-based permissions and access control in your supastarter frontend application.
We have already guided you through the process of how to protect API endpoints in your application. In this guide we will show you how you can protect routes and display UI based on typed permissions.
supastarter uses Permix with a shared @repo/permissions matrix. Prefer permission checks over raw role string comparisons.
Built-in permission keys:
admin.access— global platform admin (user.role === "admin")organization.read— any organization memberorganization.manage— organization owner/admin, or global adminorganization.delete— organization owner onlyorganization.manageBilling— organization owner/adminorganization.accessBillingPortal— organization owner only
TanStack Start wires Permix through:
- App-root
apps/saas/start.tsrequest middleware (createMiddleware().server(...)) so server-only auth/DB imports stay out of the client graph - Router hydration in
__root.tsxviagetPermixState(createServerFn, not a*.server.*module) - Client
PermixProviderandusePermissions(permix)
isOrganizationAdmin / isOrganizationOwner from @repo/auth/lib/helper remain as thin wrappers for backwards compatibility. Prefer checkPermission / usePermissions().check in new code.
Better Auth organization.* client endpoints keep Better Auth's own access control and are not covered by Permix.
Protect a route (server side)
For authenticated users
To protect a route to be only accessible for authenticated users, check the session in a route beforeLoad / server function and redirect when missing.
Note: When you are inside the authenticated SaaS app, you often don't need to check if the user is authenticated on each page, because the session is already verified by the app-level auth flow.
import { getSession } from "@auth/lib/auth-server.server";
import { createFileRoute, redirect } from "@tanstack/react-router";
import { createServerFn } from "@tanstack/react-start";
const requireSessionFn = createServerFn({
method: "GET",
strict: false,
}).handler(async () => {
const session = await getSession();
if (!session) {
throw redirect({ href: "/login" });
}
return session;
});
export const Route = createFileRoute("/_authenticated/example")({
beforeLoad: async () => {
await requireSessionFn();
},
});For platform admins
Use checkPermission with admin.access. This avoids depending on request-middleware Permix setup having completed (permix.getOrThrow(context)).
import { getSession } from "@auth/lib/auth-server.server";
import { checkPermission } from "@repo/permissions";
import { createFileRoute, redirect } from "@tanstack/react-router";
import { createServerFn } from "@tanstack/react-start";
const requireAdminAccessFn = createServerFn({
method: "GET",
strict: false,
}).handler(async () => {
const session = await getSession();
if (!session) {
throw redirect({ href: "/login" });
}
if (!checkPermission({ user: session.user }, "admin.access")) {
throw redirect({ href: "/" });
}
});
export const Route = createFileRoute("/_authenticated/_main/admin")({
beforeLoad: async () => {
await requireAdminAccessFn();
},
});For active or specific subscription
Or if you want to check for an active subscription, you can do the following:
export async function MyPremiumPage() {
const purchases = await getPurchases();
const { activePlan, hasSubscription } = createPurchasesHelper(purchases);
if (!activePlan) {
return redirect({ href: "/app" });
// or show a message to the user that they need to subscribe to the premium plan
}
// or check for a specific subscription - you don't need to check for the active plan if you use this
if (!hasSubscription("pro")) {
return (
<div>This page is only accessible for users with a pro subscription</div>
);
}
return (
<div>
This page is only accessible for users with an active subscription
</div>
);
}For organization permissions
Resolve membership for the target organization and use checkPermission with that membership role. Prefer this over isOrganizationAdmin / role string comparisons.
import { getSession } from "@auth/lib/auth-server.server";
import { checkPermission } from "@repo/permissions";
const session = await getSession();
const membershipRole =
organization.members.find((member) => member.userId === session?.user.id)
?.role ?? null;
if (
!checkPermission(
{
user: session?.user,
membershipRole,
},
"organization.manage",
)
) {
throw redirect({ href: "/app" });
}Display UI based on permissions (client side)
On the client, use usePermissions(permix) with the router-context Permix instance. __root.tsx hydrates Permix state into PermixProvider.
Note: You always want to check the permission on the server side first to avoid any security issues.
For authenticated users
import { useSession } from "@auth/hooks/use-session";
export function MyComponent() {
const { user } = useSession();
if (!user) {
return <div>You need to be logged in to access this page</div>;
}
return <div>You are logged in</div>;
}For platform admins
import { usePermissions } from "@shared/components/PermixProvider";
import { useRouteContext } from "@tanstack/react-router";
export function MyComponent() {
const { permix } = useRouteContext({ from: "__root__" });
const { check } = usePermissions(permix);
if (!check("admin.access")) {
return <div>This page is only accessible for admins</div>;
}
return <div>This page is only accessible for admins</div>;
}For active or specific subscription
export function MyComponent() {
const { activePlan, hasSubscription } = usePurchases(); // or usePurchases(organizationId) if you have enabled billing for organizations
if (!activePlan) {
return <div>You don't have an active subscription</div>;
}
if (!hasSubscription("pro")) {
return <div>You need to subscribe to the pro plan to access this page</div>;
}
return <div>You have an active subscription</div>;
}For organization permissions
import { usePermissions } from "@shared/components/PermixProvider";
import { useRouteContext } from "@tanstack/react-router";
export function MyComponent() {
const { permix } = useRouteContext({ from: "__root__" });
const { check } = usePermissions(permix);
// Active-organization checks via the hydrated Permix client
if (!check("organization.manage")) {
return <div>This page is only accessible for organization admins</div>;
}
if (!check("organization.delete")) {
return <div>Only organization owners can delete the organization</div>;
}
return <div>You can manage this organization</div>;
}When a component is keyed by a specific organizationId (not necessarily the active org), resolve that org's membership and use checkPermission instead of usePermissions().check:
import { checkPermission } from "@repo/permissions";
import { useSession } from "@auth/hooks/use-session";
const { user } = useSession();
const membershipRole = organization?.members.find(
(member) => member.userId === user?.id,
)?.role;
const canManageOrganization = checkPermission(
{
user,
membershipRole,
},
"organization.manage",
);