Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Configure Figma Enterprise features: OAuth 2.0, team management, and access control. Use when implementing OAuth flows, managing team/project access via API, or building Enterprise-level Figma integrations. Trigger with phrases like "figma enterprise", "figma OAuth", "figma team management", "figma access control", "figma SCIM".
.claude/skills/jeremylongshore-figma-enterprise-rbac/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | -2% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 101% | 0% |
| case-14 | ✗→✓ | ▲ Improved | 94% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 118% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 58% | 0% |
Figma Enterprise features accessible via the REST API: OAuth 2.0 for user-facing apps, team/project management, and the Variables API (Enterprise-only). This skill covers building OAuth integrations and managing organizational access.
typescript// Figma OAuth 2.0 Authorization Code Flow // 1. Build authorization URL function getAuthUrl(state: string): string { const params = new URLSearchParams({ client_id: process.env.FIGMA_CLIENT_ID!, redirect_uri: process.env.FIGMA_REDIRECT_URI!, scope: 'file_content:read,file_comments:write,file_variables:read', state, response_type: 'code', }); return `https://www.figma.com/oauth?${params}`; } // 2. Exchange authorization code for tokens (within 30 seconds!) async function exchangeCode(code: string): Promise<{ access_token: string; refresh_token: string; expires_in: number; user_id: string; }> { const res = await fetch('https://api.figma.com/v1/oauth/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ client_id: process.env.FIGMA_CLIENT_ID!, client_secret: process.env.FIGMA_CLIENT_SECRET!, redirect_uri: process.env.FIGMA_REDIRECT_URI!, code, grant_type: 'authorization_code', }), }); if (!res.ok) { const error = await res.text(); throw new Error(`Token exchange failed: ${res.status} ${error}`); } return res.json(); } // 3. Refresh expired tokens async function refreshAccessToken(refreshToken: string): Promise<{ access_token: string; expires_in: number; }> { const res = await fetch('https://api.figma.com/v1/oauth/refresh', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ client_id: process.env.FIGMA_CLIENT_ID!, client_secret: process.env.FIGMA_CLIENT_SECRET!, refresh_token: refreshToken, }), }); if (!res.ok) throw new Error(`Token refresh failed: ${res.status}`); return res.json(); }
typescript// Express callback handler app.get('/auth/figma/callback', async (req, res) => { const { code, state } = req.query; // Verify state matches what we sent (CSRF protection) if (state !== req.session.oauthState) { return res.status(403).json({ error: 'Invalid state parameter' }); } try { // Exchange code within 30 seconds const tokens = await exchangeCode(code as string); // Get user info with the new token const userRes = await fetch('https://api.figma.com/v1/me', { headers: { Authorization: `Bearer ${tokens.access_token}` }, }); const user = await userRes.json(); // Store tokens securely (encrypted at rest) await saveUserTokens(user.id, { accessToken: tokens.access_token, refreshToken: tokens.refresh_token, expiresAt: new Date(Date.now() + tokens.expires_in * 1000), }); res.redirect('/dashboard?connected=figma'); } catch (error) { console.error('Figma OAuth error:', error); res.redirect('/settings?error=figma_auth_failed'); } });
typescript// GET /v1/teams/:team_id/projects -- list team projects async function getTeamProjects(teamId: string, token: string) { const res = await fetch( `https://api.figma.com/v1/teams/${teamId}/projects`, { headers: { Authorization: `Bearer ${token}` } } ); return res.json(); // { projects: [{ id, name }] } } // GET /v1/projects/:project_id/files -- list project files async function getProjectFiles(projectId: string, token: string) { const res = await fetch( `https://api.figma.com/v1/projects/${projectId}/files`, { headers: { Authorization: `Bearer ${token}` } } ); return res.json(); // { files: [{ key, name, thumbnail_url, last_modified }] } } // GET /v1/teams/:team_id/components -- published components (Tier 3) async function getTeamComponents(teamId: string, token: string) { const res = await fetch( `https://api.figma.com/v1/teams/${teamId}/components`, { headers: { Authorization: `Bearer ${token}` } } ); return res.json(); // { meta: { components: [{ key, file_key, node_id, name, description }] } } } // GET /v1/teams/:team_id/styles -- published styles (Tier 3) async function getTeamStyles(teamId: string, token: string) { const res = await fetch( `https://api.figma.com/v1/teams/${teamId}/styles`, { headers: { Authorization: `Bearer ${token}` } } ); return res.json(); // { meta: { styles: [{ key, file_key, node_id, name, style_type }] } } }
typescript// GET /v1/files/:key/variables/local -- Tier 2, requires file_variables:read async function getLocalVariables(fileKey: string, token: string) { const res = await fetch( `https://api.figma.com/v1/files/${fileKey}/variables/local`, { headers: { Authorization: `Bearer ${token}` } } ); if (res.status === 403) { throw new Error('Variables API requires Figma Enterprise plan'); } return res.json(); // { meta: { variables: Record<id, Variable>, variableCollections: Record<id, Collection> } } } // GET /v1/files/:key/variables/published -- published variables async function getPublishedVariables(fileKey: string, token: string) { const res = await fetch( `https://api.figma.com/v1/files/${fileKey}/variables/published`, { headers: { Authorization: `Bearer ${token}` } } ); return res.json(); // Published variables have a subscribed_id that changes each publish } // POST /v1/files/:key/variables -- bulk create/update/delete async function updateVariables( fileKey: string, changes: VariableChanges, token: string ) { const res = await fetch( `https://api.figma.com/v1/files/${fileKey}/variables`, { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', }, body: JSON.stringify(changes), } ); return res.json(); }
typescript// Middleware that checks if user has Figma access to a resource async function requireFigmaAccess(fileKey: string) { return async (req: Request, res: Response, next: NextFunction) => { const userToken = await getUserFigmaToken(req.user.id); if (!userToken) { return res.status(403).json({ error: 'Figma account not connected' }); } // Check if user's token can access this file const check = await fetch( `https://api.figma.com/v1/files/${fileKey}?depth=1`, { headers: { Authorization: `Bearer ${userToken}` } } ); if (check.status === 403) { return res.status(403).json({ error: 'No access to this Figma file' }); } next(); }; }
| Error | Cause | Solution | |-------|-------|----------| | OAuth code expired | Exchange took >30s | Exchange immediately on callback | | Token refresh failed | Refresh token revoked | Re-authenticate user through OAuth flow | | 403 on Variables API | Not Enterprise plan | Use styles API instead (available on all plans) | | Team components empty | No published components | Publish components in Figma first |
Complete the OAuth flow locally and inspect the granted user (Steps 1-2):
bash# After the callback exchanges the code (POST /v1/oauth/token): curl -s -H "Authorization: Bearer ${FIGMA_OAUTH_TOKEN}" https://api.figma.com/v1/me \ | jq '{handle, email}' # {"handle": "ops-bot", "email": "design-infra@example.com"}
List a project's files as that user — RBAC means you only see what the user can (Step 3):
bashcurl -s -H "Authorization: Bearer ${FIGMA_OAUTH_TOKEN}" \ "https://api.figma.com/v1/projects/${PROJECT_ID}/files" | jq '.files[].name'
A 403 here is working access control, not a bug — route it through the Step 5 middleware. Variables API (Enterprise-only) usage: references/variables-api-enterprise-only.md.
For major migrations, see figma-migration-deep-dive.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 23,911 | 15,739 | -34% | 1 | 1 | 0% | 5,311 | 5,206 | -2% | 0 | 0 | — |
case-02 | pass→pass | 10,773 | 5,684 | -47% | 1 | 1 | 0% | 1,752 | 3,828 | +118% | 0 | 0 | — |
case-03 | pass→pass | 12,840 | 6,921 | -46% | 1 | 1 | 0% | 2,567 | 4,046 | +58% | 0 | 0 | — |
case-04 | pass→pass | 19,917 | 20,844 | +5% | 1 | 1 | 0% | 3,397 | 6,088 | +79% | 0 | 0 | — |
case-05 | pass→pass | 6,160 | 6,241 | +1% | 1 | 1 | 0% | 1,276 | 3,629 | +184% | 0 | 0 | — |
case-06 | fail→fail | 14,842 | 7,225 | -51% | 1 | 1 | 0% | 2,157 | 4,049 | +88% | 0 | 0 | — |
case-07 | pass→pass | 11,673 | 5,098 | -56% | 1 | 1 | 0% | 1,662 | 3,338 | +101% | 0 | 0 | — |
case-08 | pass→pass | 11,692 | 11,350 | -3% | 1 | 1 | 0% | 2,016 | 4,790 | +138% | 0 | 0 | — |
case-09 | pass→pass | 7,762 | 3,327 | -57% | 1 | 1 | 0% | 1,410 | 3,120 | +121% | 0 | 0 | — |
case-10 | pass→pass | 15,307 | 9,931 | -35% | 1 | 1 | 0% | 2,396 | 4,366 | +82% | 0 | 0 | — |
case-11 | fail→pass | 9,252 | 7,423 | -20% | 1 | 1 | 0% | 1,911 | 3,840 | +101% | 0 | 0 | — |
case-12 | pass→pass | 12,487 | 13,830 | +11% | 1 | 1 | 0% | 2,606 | 4,974 | +91% | 0 | 0 | — |
case-13 | pass→pass | 12,823 | 9,149 | -29% | 1 | 1 | 0% | 2,766 | 4,552 | +65% | 0 | 0 | — |
case-14 | fail→pass | 16,024 | 12,746 | -20% | 1 | 1 | 0% | 2,590 | 5,026 | +94% | 0 | 0 | — |
case-15 | pass→pass | 15,295 | 16,385 | +7% | 1 | 1 | 0% | 2,979 | 5,300 | +78% | 0 | 0 | — |
case-16 | pass→pass | 926,706 | 3,839 | -100% | 1 | 1 | 0% | 1,079 | 3,112 | +188% | 0 | 0 | — |
case-17 | pass→pass | 7,847 | 5,482 | -30% | 1 | 1 | 0% | 1,412 | 3,524 | +150% | 0 | 0 | — |
case-18 | pass→pass | 6,805 | 6,672 | -2% | 1 | 1 | 0% | 1,110 | 3,615 | +226% | 0 | 0 | — |
case-19 | pass→pass | 11,539 | 9,616 | -17% | 1 | 1 | 0% | 2,292 | 4,669 | +104% | 0 | 0 | — |
case-20 | pass→pass | 10,309 | 5,444 | -47% | 1 | 1 | 0% | 2,136 | 3,746 | +75% | 0 | 0 | — |
case-21 | pass→pass | 11,294 | 12,863 | +14% | 1 | 1 | 0% | 2,239 | 5,309 | +137% | 0 | 0 | — |
case-22 | pass→pass | 12,768 | 14,672 | +15% | 1 | 1 | 0% | 2,417 | 4,853 | +101% | 0 | 0 | — |
case-23 | pass→pass | 11,174 | 12,460 | +12% | 1 | 1 | 0% | 2,325 | 5,391 | +132% | 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. 23 cases were attempted. The headline lift of +13 percentage points is the difference between those two pass rates over the 23 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.