Permissions and access control
Basic access control
Of course you want to protect certain API endpoints to be only accessible by authenticated users, specific roles or even specific criteria of each user.
With supastarter you can control the access of each endpoint individually using oRPC procedures. From the context of the handler you have access to the following data:
user: The information of the authenticated user (always defined if you use a protected procedure)session: The session of the user (always defined if you use a protected procedure)
There are three kinds of procedures you can use:
publicProcedure: This procedure is accessible by everyoneprotectedProcedure: This procedure is only accessible by authenticated usersadminProcedure: This procedure is only accessible by authenticated users with theadmin.accesspermission
To protect a procedure so that only authenticated users can request it you can use the protectedProcedure like so:
import { protectedProcedure } from "../../../orpc/procedures";
export const myProtectedEndpoint = protectedProcedure
.route({
method: "GET",
path: "/my-endpoint",
})
.handler(async ({ context: { user } }) => {
return `Hello ${user.name}`;
});If someone tries to call this endpoint without being authenticated, they will receive a 401 Unauthorized error.
In case you want to have an endpoint that is available for everyone but only returns certain data for authenticated users, use the publicProcedure and check the session manually:
import { publicProcedure } from "../../../orpc/procedures";
import { auth } from "@repo/auth";
export const myPublicEndpoint = publicProcedure
.route({
method: "GET",
path: "/my-public-endpoint",
})
.handler(async ({ context }) => {
const session = await auth.api.getSession({
headers: context.headers,
});
if (session?.user) {
return `Hello ${session.user.name}`;
}
return `Hello anonymous`;
});Permix 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
The SaaS app creates a per-app Permix instance in apps/saas/plugins/permix.ts, wraps the app with PermixProvider, and keeps rules in sync via useSetupPermissions().
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.
Platform admin page
For user-scoped gates like admin.access, evaluate with checkPermission({ user }) after the session is loaded so the page does not depend on Permix setup() / isReady timing:
<script setup lang="ts">
import { checkPermission } from "@repo/permissions";
const { user, prefetchSession } = useSession();
await prefetchSession();
if (!user.value || !checkPermission({ user: user.value }, "admin.access")) {
await navigateTo("/");
}
</script>Organization UI
Use usePermissions().check for active-organization UI gates:
<script setup lang="ts">
const { check } = usePermissions();
const canManageOrganization = computed(() => check("organization.manage"));
</script>
<template>
<div v-if="canManageOrganization">Organization settings</div>
</template>Organization-level access control in oRPC
To check organization permissions in a procedure handler, resolve membership and use checkPermission:
import { checkPermission } from "@repo/permissions";
import { verifyOrganizationMembership } from "../lib/membership";
const membership = await verifyOrganizationMembership(organizationId, user.id);
if (
!membership ||
!checkPermission(
{
user,
membershipRole: membership.role,
},
"organization.manage",
)
) {
throw new ORPCError("FORBIDDEN");
}To verify organization membership only, use the verifyOrganizationMembership function from packages/api/modules/organizations/lib/membership.ts:
import { verifyOrganizationMembership } from "../lib/membership";
const membership = await verifyOrganizationMembership(organizationId, user.id);
if (!membership) {
throw new ORPCError("FORBIDDEN");
}