Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Implement enterprise role-based access control for Gamma integrations. Use when configuring team permissions, multi-tenant access, or enterprise authorization patterns. Trigger with phrases like "gamma RBAC", "gamma permissions", "gamma access control", "gamma enterprise", "gamma roles".
.claude/skills/jeremylongshore-gamma-enterprise-rbac/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 32% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 10% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 126% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 89% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 43% | 0% |
Implement role-based access control for Gamma API integrations. Gamma's API uses a single API key per workspace -- granular permissions must be implemented in your application layer. The Teams and Business plans support workspace-level collaboration with shared themes and folders.
gamma-install-auth setupGamma Workspace (1 API key)
├── Themes (shared across workspace)
├── Folders (shared across workspace)
└── Generations (tied to API key, not individual users)
Your Application Layer (you implement this):
├── Organization
│ ├── Admin (manage API key, configure themes)
│ ├── Editor (generate presentations, use templates)
│ ├── Viewer (view generated presentations, download exports)
│ └── Guest (no generation access)Key point: Gamma's API does not have per-user authentication. All API calls use the workspace API key. You must enforce per-user permissions in your application.
typescript// src/auth/gamma-roles.ts type GammaRole = "guest" | "viewer" | "editor" | "admin"; const PERMISSIONS: Record<GammaRole, string[]> = { guest: [], viewer: ["generation:view", "export:download"], editor: ["generation:view", "generation:create", "export:download", "template:use"], admin: [ "generation:view", "generation:create", "export:download", "template:use", "template:manage", "theme:manage", "settings:manage", "member:manage", ], }; function hasPermission(role: GammaRole, permission: string): boolean { return PERMISSIONS[role]?.includes(permission) ?? false; }
typescript// src/middleware/gamma-auth.ts import { Request, Response, NextFunction } from "express"; function requireGammaPermission(permission: string) { return (req: Request, res: Response, next: NextFunction) => { const user = req.user; // Set by your auth middleware if (!user) return res.status(401).json({ error: "Unauthorized" }); if (!hasPermission(user.gammaRole, permission)) { return res.status(403).json({ error: "Forbidden", required: permission, userRole: user.gammaRole, }); } next(); }; } // Usage app.post("/api/presentations", requireGammaPermission("generation:create"), async (req, res) => { const gamma = createGammaClient({ apiKey: process.env.GAMMA_API_KEY! }); const { generationId } = await gamma.generate(req.body); // Track ownership in your database await db.generations.create({ data: { generationId, userId: req.user.id, teamId: req.user.teamId }, }); res.json({ generationId }); } ); app.get("/api/presentations/:id", requireGammaPermission("generation:view"), async (req, res) => { // Only return if user owns it or is in the same team const gen = await db.generations.findFirst({ where: { generationId: req.params.id, teamId: req.user.teamId }, }); if (!gen) return res.status(404).json({ error: "Not found" }); res.json(gen); } );
typescript// src/tenant/gamma-tenant.ts // Each tenant can have their own Gamma workspace (API key) // or share a workspace with resource-level isolation interface Tenant { id: string; name: string; gammaApiKey: string; // Encrypted in database } class TenantGammaService { private clients = new Map<string, ReturnType<typeof createGammaClient>>(); getClient(tenant: Tenant) { if (!this.clients.has(tenant.id)) { this.clients.set( tenant.id, createGammaClient({ apiKey: tenant.gammaApiKey }) ); } return this.clients.get(tenant.id)!; } async generate(tenant: Tenant, userId: string, content: string, options: any = {}) { const gamma = this.getClient(tenant); const { generationId } = await gamma.generate({ content, ...options, }); // Track with tenant isolation await db.generations.create({ data: { generationId, tenantId: tenant.id, userId }, }); return { generationId }; } }
typescript// src/quota/gamma-quotas.ts interface Quota { maxGenerationsPerDay: number; maxCreditsPerMonth: number; } const ROLE_QUOTAS: Record<GammaRole, Quota> = { guest: { maxGenerationsPerDay: 0, maxCreditsPerMonth: 0 }, viewer: { maxGenerationsPerDay: 0, maxCreditsPerMonth: 0 }, editor: { maxGenerationsPerDay: 10, maxCreditsPerMonth: 500 }, admin: { maxGenerationsPerDay: 50, maxCreditsPerMonth: 5000 }, }; async function checkQuota(userId: string, role: GammaRole): Promise<boolean> { const quota = ROLE_QUOTAS[role]; if (quota.maxGenerationsPerDay === 0) return false; const todayCount = await db.generations.count({ where: { userId, createdAt: { gte: new Date(new Date().toDateString()) }, }, }); return todayCount < quota.maxGenerationsPerDay; }
typescript// src/audit/gamma-audit.ts async function auditGammaAction(entry: { userId: string; teamId: string; action: string; resourceId?: string; metadata?: Record<string, any>; }) { await db.auditLog.create({ data: { ...entry, timestamp: new Date(), service: "gamma", }, }); } // Usage await auditGammaAction({ userId: req.user.id, teamId: req.user.teamId, action: "generation.create", resourceId: generationId, metadata: { outputFormat: "presentation", credits: result.creditsUsed }, });
| Permission | Guest | Viewer | Editor | Admin | |------------|-------|--------|--------|-------| | View presentations | No | Yes | Yes | Yes | | Download exports | No | Yes | Yes | Yes | | Create generations | No | No | Yes | Yes | | Use templates | No | No | Yes | Yes | | Manage themes/folders | No | No | No | Yes | | Manage team members | No | No | No | Yes | | Configure API key | No | No | No | Yes |
| Issue | Cause | Solution | |-------|-------|----------| | 403 Forbidden | Insufficient role | Check user's gammaRole assignment | | Cross-tenant access | Wrong API key | Verify tenant isolation in getClient() | | Quota exceeded | Too many generations | Show remaining quota, wait for reset | | Privilege escalation | Missing role check | Verify middleware on all routes |
Proceed to gamma-migration-deep-dive for platform migration.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 19,099 | 16,774 | -12% | 1 | 1 | 0% | 3,420 | 4,531 | +32% | 0 | 0 | — |
case-02 | fail→fail | 20,672 | 25,126 | +22% | 1 | 1 | 0% | 4,462 | 6,470 | +45% | 0 | 0 | — |
case-03 | fail→pass | 22,320 | 15,655 | -30% | 1 | 1 | 0% | 4,771 | 5,261 | +10% | 0 | 0 | — |
case-04 | fail→pass | 19,276 | 24,981 | +30% | 1 | 1 | 0% | 2,625 | 5,928 | +126% | 0 | 0 | — |
case-05 | fail→pass | 15,980 | 9,341 | -42% | 1 | 1 | 0% | 2,111 | 3,980 | +89% | 0 | 0 | — |
case-06 | pass→pass | 19,228 | 16,355 | -15% | 1 | 1 | 0% | 3,715 | 5,329 | +43% | 0 | 0 | — |
case-07 | fail→fail | 17,385 | 14,338 | -18% | 1 | 1 | 0% | 2,521 | 3,754 | +49% | 0 | 0 | — |
case-08 | fail→fail | 14,800 | 17,405 | +18% | 1 | 1 | 0% | 3,051 | 4,437 | +45% | 0 | 0 | — |
case-09 | fail→pass | 18,032 | 10,878 | -40% | 1 | 1 | 0% | 2,102 | 3,016 | +43% | 0 | 0 | — |
case-10 | pass→pass | 10,205 | 12,709 | +25% | 1 | 1 | 0% | 1,705 | 3,238 | +90% | 0 | 0 | — |
case-11 | pass→pass | 11,770 | 9,547 | -19% | 1 | 1 | 0% | 1,134 | 2,714 | +139% | 0 | 0 | — |
case-12 | fail→pass | 19,545 | 7,281 | -63% | 1 | 1 | 0% | 2,957 | 3,419 | +16% | 0 | 0 | — |
case-13 | pass→pass | 10,832 | 8,950 | -17% | 1 | 1 | 0% | 1,225 | 2,684 | +119% | 0 | 0 | — |
case-14 | fail→pass | 19,930 | 14,948 | -25% | 1 | 1 | 0% | 3,290 | 4,089 | +24% | 0 | 0 | — |
case-15 | pass→pass | 9,538 | 7,496 | -21% | 1 | 1 | 0% | 2,178 | 3,494 | +60% | 0 | 0 | — |
case-16 | pass→pass | 10,923 | 7,826 | -28% | 1 | 1 | 0% | 2,429 | 3,610 | +49% | 0 | 0 | — |
case-17 | fail→pass | 11,242 | 3,961 | -65% | 1 | 1 | 0% | 2,332 | 2,895 | +24% | 0 | 0 | — |
case-18 | fail→fail | 10,514 | 8,681 | -17% | 1 | 1 | 0% | 2,401 | 3,451 | +44% | 0 | 0 | — |
case-19 | fail→pass | 13,820 | 4,084 | -70% | 1 | 1 | 0% | 1,931 | 2,821 | +46% | 0 | 0 | — |
case-20 | fail→pass | 13,595 | 15,574 | +15% | 1 | 1 | 0% | 3,232 | 4,131 | +28% | 0 | 0 | — |
case-21 | fail→pass | 14,228 | 10,658 | -25% | 1 | 1 | 0% | 2,507 | 3,988 | +59% | 0 | 0 | — |
case-22 | fail→fail | 28,610 | 32,758 | +14% | 1 | 1 | 0% | 3,880 | 7,923 | +104% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +50 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.