Documentation
Docs

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 member
  • organization.manage — organization owner/admin, or global admin
  • organization.delete — organization owner only
  • organization.manageBilling — organization owner/admin
  • organization.accessBillingPortal — organization owner only

TanStack Start wires Permix through:

  • App-root apps/saas/start.ts request middleware (createMiddleware().server(...)) so server-only auth/DB imports stay out of the client graph
  • Router hydration in __root.tsx via getPermixState (createServerFn, not a *.server.* module)
  • Client PermixProvider and usePermissions(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",
);