Smithery Logo
MCPsSkillsDocsPricing
Login
Smithery Logo

Accelerating the Agent Economy

Resources

DocumentationPrivacy PolicySystem Status

Company

PricingAboutBlog

Connect

© 2026 Smithery. All rights reserved.

    ToonVos

    permissions

    ToonVos/permissions
    Security
    1

    About

    SKILL.md

    Install

    Install via Skills CLI

    or add to your agent
    • Claude Code
      Claude Code
    • Codex
      Codex
    • OpenClaw
      OpenClaw
    • Cursor
      Cursor
    • Amp
      Amp
    • GitHub Copilot
      GitHub Copilot
    • Gemini CLI
      Gemini CLI
    • Kilo Code
      Kilo Code
    • Junie
      Junie
    • Replit
      Replit
    • Windsurf
      Windsurf
    • Cline
      Cline
    • Continue
      Continue
    • OpenCode
      OpenCode
    • OpenHands
      OpenHands
    • Roo Code
      Roo Code
    • Augment
      Augment
    • Goose
      Goose
    • Trae
      Trae
    • Zencoder
      Zencoder
    • Antigravity
      Antigravity
    ├─
    ├─
    └─

    About

    Multi-tenant permission checking for Wasp applications. Use when implementing authorization, access control, or role-based permissions...

    SKILL.md

    Permissions Skill

    Quick Reference

    When to use this skill:

    • Implementing permission checks in operations
    • Setting up role-based access control
    • Working with multi-tenant data (organizations/departments)
    • Checking organization or department access
    • Filtering data by user permissions
    • Enforcing hierarchical access (parent/child departments)

    Key patterns:

    • canAccessDocument() - Check if user can view document
    • canEditDocument() - Check if user can edit document
    • canDeleteDocument() - Check if user can delete document
    • getUserOrgRole() - Get user's role in organization
    • getUserRoleInDepartment() - Get user's role in department
    • isDepartmentManager() - Check if user manages department
    • isOrgAdmin() - Check if user is org owner/admin

    Multi-Tenancy Architecture

    Structure:

    Organization (many) → Departments (hierarchical tree via parentId)
                              ↓
                        Users ↔ Departments (many-to-many via UserDepartment)
                              ↓
                        DepartmentRole: MANAGER | MEMBER | VIEWER
                        OrganizationRole: OWNER | ADMIN | MEMBER
    

    Key concepts:

    • Organizations contain multiple departments
    • Departments can have parent/child relationships (hierarchical)
    • Users belong to departments via UserDepartment junction table
    • Each user has a role per department (MANAGER, MEMBER, or VIEWER)
    • Users can have roles in multiple departments
    • Organization-level roles (OWNER, ADMIN) grant access to all departments

    Database Schema

    model Organization {
      id          String       @id @default(uuid())
      name        String
      departments Department[]
      members     OrganizationMember[]
    }
    
    model OrganizationMember {
      id             String       @id @default(uuid())
      userId         String
      user           User         @relation(fields: [userId], references: [id])
      organizationId String
      organization   Organization @relation(fields: [organizationId], references: [id])
      role           OrganizationRole
    
      @@unique([userId, organizationId])
    }
    
    enum OrganizationRole {
      OWNER
      ADMIN
      MEMBER
    }
    
    model Department {
      id              String           @id @default(uuid())
      name            String
      organizationId  String
      organization    Organization     @relation(fields: [organizationId], references: [id])
      parentId        String?
      parent          Department?      @relation("DepartmentHierarchy", fields: [parentId], references: [id])
      children        Department[]     @relation("DepartmentHierarchy")
      userDepartments UserDepartment[]
    }
    
    model UserDepartment {
      id           String         @id @default(uuid())
      userId       String
      user         User           @relation(fields: [userId], references: [id])
      departmentId String
      department   Department     @relation(fields: [departmentId], references: [id])
      role         DepartmentRole
    
      @@unique([userId, departmentId])
    }
    
    enum DepartmentRole {
      MANAGER
      MEMBER
      VIEWER
    }
    

    Core Permission Helpers

    Organization-Level Permissions

    import { HttpError } from "wasp/server";
    
    /**
     * Get user's role in organization
     * @returns 'OWNER' | 'ADMIN' | 'MEMBER' | 'NONE'
     */
    async function getUserOrgRole(
      userId: string,
      organizationId: string,
      context,
    ): Promise<string> {
      const membership = await context.entities.OrganizationMember.findUnique({
        where: {
          userId_organizationId: {
            userId,
            organizationId,
          },
        },
      });
    
      return membership?.role || "NONE";
    }
    
    /**
     * Check if user is organization owner or admin
     */
    async function isOrgAdmin(
      userId: string,
      organizationId: string,
      context,
    ): Promise<boolean> {
      const role = await getUserOrgRole(userId, organizationId, context);
      return ["OWNER", "ADMIN"].includes(role);
    }
    
    /**
     * Check if user can access organization
     */
    async function canAccessOrganization(
      userId: string,
      organizationId: string,
      context,
    ): Promise<boolean> {
      const role = await getUserOrgRole(userId, organizationId, context);
      return role !== "NONE";
    }
    

    Department-Level Permissions

    /**
     * Get user's role in specific department
     * @returns 'MANAGER' | 'MEMBER' | 'VIEWER' | null
     */
    async function getUserRoleInDepartment(
      userId: string,
      departmentId: string,
      context,
    ): Promise<string | null> {
      const membership = await context.entities.UserDepartment.findUnique({
        where: {
          userId_departmentId: {
            userId,
            departmentId,
          },
        },
      });
    
      return membership?.role || null;
    }
    
    /**
     * Check if user is department manager
     */
    async function isDepartmentManager(
      userId: string,
      departmentId: string,
      context,
    ): Promise<boolean> {
      const role = await getUserRoleInDepartment(userId, departmentId, context);
      return role === "MANAGER";
    }
    
    /**
     * Check if user can access department
     * Includes hierarchical access (parent departments)
     */
    async function canAccessDepartment(
      userId: string,
      departmentId: string,
      context,
    ): Promise<boolean> {
      // Get department with parent chain
      const department = await context.entities.Department.findUnique({
        where: { id: departmentId },
        include: { parent: true },
      });
    
      if (!department) return false;
    
      // Check organization access
      const hasOrgAccess = await canAccessOrganization(
        userId,
        department.organizationId,
        context,
      );
      if (!hasOrgAccess) return false;
    
      // Org admins can access all departments
      const isAdmin = await isOrgAdmin(userId, department.organizationId, context);
      if (isAdmin) return true;
    
      // Check direct membership
      const role = await getUserRoleInDepartment(userId, departmentId, context);
      if (role) return true;
    
      // Check parent department membership (hierarchical)
      if (department.parentId) {
        return await canAccessDepartment(userId, department.parentId, context);
      }
    
      return false;
    }
    

    Resource-Level Permissions (Task Example)

    /**
     * Check if user can access document
     * Access granted if:
     * - User is the author
     * - User is org OWNER/ADMIN
     * - User is in department (any role: MANAGER, MEMBER, VIEWER)
     */
    async function canAccessDocument(
      userId: string,
      a3: Document,
      context,
    ): Promise<boolean> {
      // 1. Author can always access
      if (taskRecord.authorId === userId) return true;
    
      // 2. Org admins can access
      const orgRole = await getUserOrgRole(userId, a3.organizationId, context);
      if (["OWNER", "ADMIN"].includes(orgRole)) return true;
    
      // 3. Department members can access (any role)
      const deptRole = await getUserRoleInDepartment(
        userId,
        a3.departmentId,
        context,
      );
      return deptRole !== null;
    }
    
    /**
     * Check if user can edit document
     * Edit permissions:
     * - Author can edit
     * - Org OWNER/ADMIN can edit
     * - Department MANAGER can edit
     * - Department MEMBER can edit their own
     * - VIEWER cannot edit
     */
    async function canEditDocument(
      userId: string,
      a3: Document,
      context,
    ): Promise<boolean> {
      // Author can always edit
      if (taskRecord.authorId === userId) return true;
    
      // Org admins can edit
      const orgRole = await getUserOrgRole(userId, a3.organizationId, context);
      if (["OWNER", "ADMIN"].includes(orgRole)) return true;
    
      // Department managers can edit
      const deptRole = await getUserRoleInDepartment(
        userId,
        a3.departmentId,
        context,
      );
      if (deptRole === "MANAGER") return true;
    
      // Members cannot edit others' A3s
      // Viewers cannot edit
      return false;
    }
    
    /**
     * Check if user can delete document
     * Delete permissions:
     * - Author can delete
     * - Org OWNER/ADMIN can delete
     * - Department MANAGER can delete
     */
    async function canDeleteDocument(
      userId: string,
      a3: Document,
      context,
    ): Promise<boolean> {
      // Author can delete
      if (taskRecord.authorId === userId) return true;
    
      // Org admins can delete
      const orgRole = await getUserOrgRole(userId, a3.organizationId, context);
      if (["OWNER", "ADMIN"].includes(orgRole)) return true;
    
      // Department managers can delete
      const deptRole = await getUserRoleInDepartment(
        userId,
        a3.departmentId,
        context,
      );
      if (deptRole === "MANAGER") return true;
    
      return false;
    }
    

    Usage in Operations

    Standard Operation Pattern

    ALWAYS follow this order:

    1. Auth check (401)
    2. Fetch resource
    3. Existence check (404)
    4. Permission check (403)
    5. Validation (400)
    6. Perform operation

    Query with Permission Check

    import type { GetDocument } from "wasp/server/operations";
    import type { Document } from "wasp/entities";
    import { HttpError } from "wasp/server";
    
    export const getDocument: GetDocument<{ id: string }, Document> = async (
      args,
      context,
    ) => {
      // 1. Auth check
      if (!context.user) throw new HttpError(401);
    
      // 2. Fetch resource
      const taskRecord = await context.entities.Document.findUnique({
        where: { id: args.id },
        include: {
          author: { select: { id: true, username: true } },
          department: true,
          organization: true,
        },
      });
    
      // 3. Existence check
      if (!taskRecord) throw new HttpError(404, "document not found");
    
      // 4. Permission check using helper
      const hasAccess = await canAccessDocument(context.user.id, a3, context);
      if (!hasAccess) {
        throw new HttpError(403, "Not authorized to access this document");
      }
    
      // 5. Return resource
      return taskRecord;
    };
    

    Query with Permission Filtering

    import type { GetDocuments } from "wasp/server/operations";
    import type { Document } from "wasp/entities";
    
    export const getDocuments: GetDocuments<void, Document[]> = async (
      args,
      context,
    ) => {
      if (!context.user) throw new HttpError(401);
    
      // Get all departments user has access to
      const userDepts = await context.entities.UserDepartment.findMany({
        where: { userId: context.user.id },
      });
    
      const deptIds = userDepts.map((ud) => ud.departmentId);
    
      // Return A3s from accessible departments
      return context.entities.Document.findMany({
        where: {
          OR: [
            // Own documents
            { authorId: context.user.id },
            // Department documents
            { departmentId: { in: deptIds } },
          ],
        },
        include: {
          department: true,
          author: { select: { id: true, username: true } },
        },
        orderBy: { createdAt: "desc" },
      });
    };
    

    Action with Edit Permission Check

    import type { UpdateA3 } from "wasp/server/operations";
    
    export const updateA3: UpdateA3 = async (args, context) => {
      // 1. Auth check
      if (!context.user) throw new HttpError(401);
    
      // 2. Fetch resource
      const taskRecord = await context.entities.Document.findUnique({
        where: { id: args.id },
      });
    
      // 3. Existence check
      if (!taskRecord) throw new HttpError(404, "document not found");
    
      // 4. Permission check (can edit?)
      const canEdit = await canEditDocument(context.user.id, a3, context);
      if (!canEdit) {
        throw new HttpError(403, "Not authorized to edit this document");
      }
    
      // 5. Update
      return context.entities.Document.update({
        where: { id: args.id },
        data: args.data,
      });
    };
    

    Action with Manager-Only Permission

    import type { DeleteA3 } from "wasp/server/operations";
    
    export const deleteA3: DeleteA3 = async (args, context) => {
      if (!context.user) throw new HttpError(401);
    
      const taskRecord = await context.entities.Document.findUnique({
        where: { id: args.id },
      });
    
      if (!taskRecord) throw new HttpError(404, "document not found");
    
      // Only MANAGER can delete
      const canDelete = await canDeleteDocument(context.user.id, a3, context);
      if (!canDelete) {
        throw new HttpError(403, "Only department managers can delete documents");
      }
    
      return context.entities.Document.delete({
        where: { id: args.id },
      });
    };
    

    Create Resource with Role Check

    import type { CreateA3 } from "wasp/server/operations";
    
    export const createA3: CreateA3 = async (args, context) => {
      if (!context.user) throw new HttpError(401);
    
      // Check user has at least MEMBER role in department
      const role = await getUserRoleInDepartment(
        context.user.id,
        args.departmentId,
        context,
      );
    
      if (role !== "MANAGER" && role !== "MEMBER") {
        throw new HttpError(403, "Need MEMBER or MANAGER role to create documents");
      }
    
      return context.entities.Document.create({
        data: {
          ...args.data,
          departmentId: args.departmentId,
          authorId: context.user.id,
        },
      });
    };
    

    Advanced Patterns

    Batch Permission Checks

    /**
     * Get all permissions for Task at once
     * Useful for UI rendering (show/hide buttons)
     */
    async function getDocumentPermissions(
      userId: string,
      a3: Document,
      context,
    ): Promise<{
      canView: boolean;
      canEdit: boolean;
      canDelete: boolean;
      canShare: boolean;
    }> {
      const [canView, canEdit, canDelete] = await Promise.all([
        canAccessDocument(userId, a3, context),
        canEditDocument(userId, a3, context),
        canDeleteDocument(userId, a3, context),
      ]);
    
      return {
        canView,
        canEdit,
        canDelete,
        canShare: canEdit, // Share requires edit permission
      };
    }
    

    Filter Multiple Resources

    /**
     * Filter Task IDs to only those user can access
     * More efficient than checking one by one
     */
    async function filterAccessibleA3s(
      userId: string,
      a3Ids: string[],
      context,
    ): Promise<string[]> {
      const a3Documents = await context.entities.Document.findMany({
        where: { id: { in: a3Ids } },
      });
    
      const accessChecks = await Promise.all(
        a3Documents.map(async (a3) => ({
          id: a3.id,
          hasAccess: await canAccessDocument(userId, a3, context),
        })),
      );
    
      return accessChecks
        .filter((check) => check.hasAccess)
        .map((check) => check.id);
    }
    

    Query-Level Filtering (Optimal)

    /**
     * Get all documents user can access
     * Uses database-level filtering for efficiency
     */
    async function getAccessibleDocuments(
      userId: string,
      context,
    ): Promise<Document[]> {
      // Get user's organizations
      const orgMemberships = await context.entities.OrganizationMember.findMany({
        where: { userId },
      });
      const orgIds = orgMemberships.map((m) => m.organizationId);
    
      // Get user's departments
      const deptMemberships = await context.entities.UserDepartment.findMany({
        where: { userId },
      });
      const deptIds = deptMemberships.map((m) => m.departmentId);
    
      // Query with permission filter
      return await context.entities.Document.findMany({
        where: {
          OR: [
            // Own documents
            { authorId: userId },
            // Organization documents (if admin)
            {
              AND: [
                { organizationId: { in: orgIds } },
                {
                  organization: {
                    members: {
                      some: {
                        userId,
                        role: { in: ["OWNER", "ADMIN"] },
                      },
                    },
                  },
                },
              ],
            },
            // Department documents
            { departmentId: { in: deptIds } },
          ],
        },
        include: {
          author: { select: { id: true, username: true } },
          department: true,
          organization: true,
        },
        orderBy: { updatedAt: "desc" },
      });
    }
    

    Permission Patterns by Role

    Organization Roles

    OWNER:

    • Full organization access
    • Can manage all departments
    • Can edit/delete all resources
    • Can manage organization settings
    • Can add/remove admins

    ADMIN:

    • Full organization access
    • Can manage all departments
    • Can edit/delete all resources
    • Cannot modify owner permissions

    MEMBER:

    • Basic organization access
    • Access determined by department roles
    • Cannot manage organization settings

    Department Roles

    MANAGER:

    • View all department resources
    • Edit all department resources
    • Delete department resources
    • Manage department members
    • Access child departments (hierarchical)

    MEMBER:

    • View department resources
    • Edit own resources
    • Create new resources
    • Cannot delete resources (manager only)
    • Cannot manage members (manager only)

    VIEWER:

    • View department resources
    • Cannot edit resources
    • Cannot create resources
    • Cannot delete resources
    • Cannot manage members

    Common Permission Patterns

    Owner Can Edit Their Own

    // Users can edit their own resources, managers can edit all
    export const updateA3 = async (args, context) => {
      if (!context.user) throw new HttpError(401);
    
      const taskRecord = await context.entities.Document.findUnique({
        where: { id: args.id },
      });
      if (!taskRecord) throw new HttpError(404);
    
      const isOwner = a3.authorId === context.user.id;
      const isManager = await isDepartmentManager(
        context.user.id,
        a3.departmentId,
        context,
      );
    
      if (!isOwner && !isManager) {
        throw new HttpError(403, "Can only edit your own documents");
      }
    
      return context.entities.Document.update({
        where: { id: args.id },
        data: args.data,
      });
    };
    

    Manager-Only Actions

    // Only department managers can perform this action
    export const archiveA3 = async (args, context) => {
      if (!context.user) throw new HttpError(401);
    
      const taskRecord = await context.entities.Document.findUnique({
        where: { id: args.id },
      });
      if (!taskRecord) throw new HttpError(404);
    
      const isManager = await isDepartmentManager(
        context.user.id,
        a3.departmentId,
        context,
      );
    
      if (!isManager) {
        throw new HttpError(403, "Only department managers can archive documents");
      }
    
      return context.entities.Document.update({
        where: { id: args.id },
        data: { status: "ARCHIVED" },
      });
    };
    

    Hierarchical Department Access

    /**
     * Get all child departments recursively
     */
    async function getChildDepartments(deptId: string, context): Promise<string[]> {
      const dept = await context.entities.Department.findUnique({
        where: { id: deptId },
        include: {
          children: {
            include: { children: true },
          },
        },
      });
    
      if (!dept) return [];
    
      const childIds: string[] = [];
    
      function collectChildren(dept: any) {
        if (dept.children) {
          dept.children.forEach((child) => {
            childIds.push(child.id);
            collectChildren(child);
          });
        }
      }
    
      collectChildren(dept);
      return [deptId, ...childIds];
    }
    

    Critical Rules

    DO:

    • ✅ Check permissions AFTER auth and existence checks
    • ✅ Use permission helpers for consistency
    • ✅ Filter queries by accessible departments
    • ✅ Include VIEWER role in read operations
    • ✅ Check role hierarchy (OWNER > ADMIN > MANAGER > MEMBER > VIEWER)
    • ✅ Handle hierarchical departments (parent access)
    • ✅ Enforce permissions server-side ALWAYS
    • ✅ Return 403 for permission failures (not 404)

    NEVER:

    • ❌ Skip permission checks in operations
    • ❌ Hardcode role checks (use helpers)
    • ❌ Forget VIEWER role in read operations
    • ❌ Allow clients to bypass permissions
    • ❌ Trust client-side permission checks (cosmetic only!)
    • ❌ Return 404 when resource exists but user lacks permission (use 403)
    • ❌ Implement permission logic in client code

    HTTP Status Code Usage

    Permission-related status codes:

    • 401 Unauthorized - Not authenticated (!context.user)
    • 403 Forbidden - Authenticated but lacks permission
    • 404 Not Found - Resource doesn't exist OR user lacks permission to know it exists

    Best practice:

    • Use 403 when you want user to know resource exists but they can't access it
    • Use 404 when you want to hide resource existence from unauthorized users
    • Always use 401 for missing authentication

    Client-Side Usage

    // React component using permission helpers
    import { useQuery } from 'wasp/client/operations'
    import { getDocument, getDocumentPermissions } from 'wasp/client/operations'
    
    function DocumentPage({ a3Id }: { a3Id: string }) {
      // Fetch document
      const { data: a3, isLoading } = useQuery(getDocument, { id: docId })
    
      // Fetch permissions
      const { data: permissions } = useQuery(getDocumentPermissions, { docId })
    
      if (isLoading) return <div>Loading...</div>
      if (!taskRecord) return <div>Not found</div>
    
      return (
        <div>
          <h1>{a3.title}</h1>
    
          {/* Conditionally render based on permissions */}
          {permissions?.canEdit && (
            <button onClick={() => editA3()}>Edit</button>
          )}
    
          {permissions?.canDelete && (
            <button onClick={() => deleteA3()}>Delete</button>
          )}
    
          {permissions?.canShare && (
            <button onClick={() => shareA3()}>Share</button>
          )}
    
          {!permissions?.canEdit && (
            <div className="text-gray-500">Read-only access</div>
          )}
        </div>
      )
    }
    

    Remember: Client-side checks are for UX only. Always enforce permissions server-side.

    References

    Complete implementation examples:

    • .claude/templates/permission-helpers.ts (623 lines)
      • Lines 1-150: Core helpers
      • Lines 151-330: Organization/department helpers
      • Lines 331-427: Usage in operations
      • Lines 428-539: Advanced patterns
      • Lines 540-583: Client-side usage

    Related documentation:

    • CLAUDE.md#architecture - Multi-tenancy architecture overview
    • CLAUDE.md#error-handling - HTTP status codes and error patterns
    • .claude/templates/operations-patterns.ts - Complete operation examples
    Recommended Servers
    WorkOS
    WorkOS
    InstantDB
    InstantDB
    Vercel
    Vercel
    Repository
    toonvos/empty-opensaas
    Files