Help us improve
Share bugs, ideas, or general feedback.
From shopify-pack
Implements RBAC for Shopify Plus apps via staff permissions, multi-location management, and organization features. Provides TypeScript code for GraphQL queries and role mapping.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --plugin shopify-packHow this skill is triggered — by the user, by Claude, or both
Slash command
/shopify-pack:shopify-enterprise-rbacThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Implement role-based access control for Shopify Plus apps using Shopify's staff member permissions, multi-location features, and Organization-level access.
Installs and configures Shopify app authentication with OAuth, session tokens, and @shopify/shopify-api SDK for API access in Node.js apps.
Sets up Shopify CLI auth and Admin API access token for a store: install CLI, login, create custom app with scopes, store token securely, verify with GraphQL. For store connections or auth issues.
Manages Saleor customers and staff including accounts, registration, addresses, permissions, and authentication via GraphQL mutations. Use for user management in Saleor apps.
Share bugs, ideas, or general feedback.
Implement role-based access control for Shopify Plus apps using Shopify's staff member permissions, multi-location features, and Organization-level access.
read_users scope for querying staff permissions// Query staff members and their permissions via GraphQL
const STAFF_QUERY = `{
staffMembers(first: 50) {
edges {
node {
id
email
firstName
lastName
isShopOwner
active
locale
permissions: accessScopes {
handle
description
}
}
}
}
}`;
// Staff permissions match app access scopes:
// "read_products", "write_products", "read_orders", etc.
// A staff member can only use app features matching their store permissions
Map Shopify staff permissions to your app's roles:
type AppRole = "admin" | "manager" | "viewer" | "fulfillment";
interface RoleMapping {
role: AppRole;
requiredScopes: string[];
allowedActions: string[];
}
const ROLE_MAPPINGS: RoleMapping[] = [
{
role: "admin",
requiredScopes: ["write_products", "write_orders", "write_customers"],
allowedActions: ["*"],
},
{
role: "manager",
requiredScopes: ["write_products", "read_orders"],
allowedActions: ["manage_products", "view_orders", "view_analytics"],
},
{
role: "fulfillment",
requiredScopes: ["read_orders", "write_fulfillments"],
allowedActions: ["view_orders", "create_fulfillment", "update_tracking"],
},
{
role: "viewer",
requiredScopes: ["read_products"],
allowedActions: ["view_products", "view_analytics"],
},
];
function determineRole(staffScopes: string[]): AppRole {
// Find the highest-privilege role the staff member qualifies for
for (const mapping of ROLE_MAPPINGS) {
if (mapping.requiredScopes.every((s) => staffScopes.includes(s))) {
return mapping.role;
}
}
return "viewer"; // fallback
}
function canPerformAction(role: AppRole, action: string): boolean {
const mapping = ROLE_MAPPINGS.find((m) => m.role === role);
if (!mapping) return false;
return mapping.allowedActions.includes("*") || mapping.allowedActions.includes(action);
}
// In an embedded Shopify app, the session token contains the staff member info
import { authenticate } from "../shopify.server";
// Remix loader with permission check
export async function loader({ request }: LoaderFunctionArgs) {
const { admin, session } = await authenticate.admin(request);
// session.onlineAccessInfo contains staff permissions for online tokens
const staffInfo = session.onlineAccessInfo;
if (!staffInfo) {
// Offline token — no per-user permissions available
return json({ role: "admin" });
}
const scopes = staffInfo.associated_user_scope.split(",");
const role = determineRole(scopes);
// Check permission for this specific page
if (!canPerformAction(role, "view_orders")) {
throw new Response("Forbidden", { status: 403 });
}
return json({ role, user: staffInfo.associated_user });
}
// Shopify Plus stores can have multiple locations
// Control which locations a staff member can access
const LOCATIONS_QUERY = `{
locations(first: 50) {
edges {
node {
id
name
isActive
address {
city
province
country
}
fulfillmentService {
serviceName
}
}
}
}
}`;
// Restrict operations to authorized locations
interface LocationPermission {
locationId: string;
canFulfill: boolean;
canAdjustInventory: boolean;
canViewOrders: boolean;
}
async function checkLocationAccess(
userId: string,
locationId: string,
action: "fulfill" | "adjust_inventory" | "view_orders"
): Promise<boolean> {
const permissions = await db.locationPermissions.findFirst({
where: { userId, locationId },
});
if (!permissions) return false;
switch (action) {
case "fulfill": return permissions.canFulfill;
case "adjust_inventory": return permissions.canAdjustInventory;
case "view_orders": return permissions.canViewOrders;
default: return false;
}
}
// Shopify Plus Organization features (multi-store management)
// Access via the Organization API
const ORG_STORES_QUERY = `{
organizationStores(first: 50) {
edges {
node {
id
name
shopDomain
plan {
displayName
}
staff(first: 10) {
edges {
node {
email
role
}
}
}
}
}
}
}`;
// Organization-level roles:
// - Organization admin: full access to all stores
// - Store-level admin: full access to assigned stores
// - Store-level staff: permission-based access
interface AccessAuditEntry {
timestamp: Date;
userId: string;
userEmail: string;
role: AppRole;
action: string;
resource: string;
shopDomain: string;
locationId?: string;
allowed: boolean;
ipAddress?: string;
}
async function auditAccess(entry: AccessAuditEntry): Promise<void> {
await db.accessAudit.create({ data: entry });
// Alert on denied access attempts
if (!entry.allowed) {
console.warn(
`[ACCESS DENIED] ${entry.userEmail} attempted ${entry.action} ` +
`on ${entry.resource} in ${entry.shopDomain}`
);
}
}
| Issue | Cause | Solution |
|---|---|---|
No onlineAccessInfo | Using offline token | Use online access tokens for per-user permissions |
| Staff can't access feature | Merchant restricted their permissions | Staff must request access from store owner |
| Organization API 403 | Not on Shopify Plus | Organization features require Plus plan |
| Location not found | Location deactivated | Query active locations before operations |
// Remix action with permission guard
export async function action({ request }: ActionFunctionArgs) {
const { admin, session } = await authenticate.admin(request);
const role = determineRole(
session.onlineAccessInfo?.associated_user_scope?.split(",") || []
);
if (!canPerformAction(role, "manage_products")) {
return json({ error: "Insufficient permissions" }, { status: 403 });
}
// ... perform the action
}
For major migrations, see shopify-migration-deep-dive.