--- name: webflow-enterprise-rbac description: "Configure Webflow enterprise access control \u2014 OAuth 2.0 app authorization,\n\ scope-based RBAC, per-site token isolation, workspace member management,\nand audit\ \ logging for compliance.\nTrigger with phrases like \"webflow RBAC\", \"webflow\ \ enterprise\",\n\"webflow roles\", \"webflow permissions\", \"webflow OAuth scopes\"\ ,\n\"webflow access control\", \"webflow workspace members\".\n" allowed-tools: Read, Write, Edit version: 1.0.0 license: MIT author: Jeremy Longshore tags: - saas - design - no-code - webflow compatibility: Designed for Claude Code --- # Webflow Enterprise RBAC ## Overview Enterprise-grade access control for Webflow Data API v2 integrations. Uses Webflow's OAuth 2.0 scope system, per-site token isolation, and application-level RBAC to enforce least privilege across teams and environments. ## Prerequisites - Webflow workspace (Core plan or higher for multiple members) - OAuth 2.0 Data Client App (for user-authorized access) - Understanding of Webflow scopes ## Webflow's Native Access Model Webflow provides three levels of access control: | Level | Mechanism | Granularity | |-------|-----------|-------------| | **Workspace tokens** | API token | All sites in workspace | | **Site tokens** | API token | Single site | | **OAuth tokens** | OAuth 2.0 authorization | User-authorized scopes per site | ## Scope-Based Permission Model Webflow scopes map directly to API access: | Scope | Read Access | Write Access | |-------|-------------|--------------| | `sites:read` | List/get sites | — | | `sites:write` | — | Publish sites | | `cms:read` | List collections, read items | — | | `cms:write` | — | Create/update/delete CMS items | | `pages:read` | List/get pages | — | | `pages:write` | — | Update page settings | | `forms:read` | List forms, read submissions | — | | `ecommerce:read` | List products, orders, inventory | — | | `ecommerce:write` | — | Create products, fulfill orders | | `custom_code:read` | List registered scripts | — | | `custom_code:write` | — | Register/apply custom code | ## Instructions ### Step 1: Define Application Roles Map your application roles to Webflow scope sets: ```typescript enum AppRole { ContentViewer = "content_viewer", ContentEditor = "content_editor", SiteAdmin = "site_admin", EcommerceManager = "ecommerce_manager", FormProcessor = "form_processor", } // Map roles to required Webflow OAuth scopes const ROLE_SCOPES: Record = { [AppRole.ContentViewer]: ["sites:read", "cms:read", "pages:read"], [AppRole.ContentEditor]: ["sites:read", "cms:read", "cms:write", "pages:read"], [AppRole.SiteAdmin]: [ "sites:read", "sites:write", "cms:read", "cms:write", "pages:read", "pages:write", "custom_code:read", "custom_code:write", ], [AppRole.EcommerceManager]: [ "sites:read", "ecommerce:read", "ecommerce:write", "cms:read", // Products are CMS collections ], [AppRole.FormProcessor]: ["sites:read", "forms:read"], }; ``` ### Step 2: OAuth App with Role-Based Scopes Request only the scopes needed for the user's role: ```typescript function getAuthorizationUrl(role: AppRole, state: string): string { const scopes = ROLE_SCOPES[role]; const scopeString = scopes.join(" "); return ( `https://webflow.com/oauth/authorize` + `?client_id=${process.env.WEBFLOW_CLIENT_ID}` + `&response_type=code` + `&redirect_uri=${encodeURIComponent(process.env.REDIRECT_URI!)}` + `&scope=${encodeURIComponent(scopeString)}` + `&state=${state}` // CSRF protection ); } // Initiate OAuth flow with role-appropriate scopes app.get("/auth/webflow", (req, res) => { const role = req.user.appRole as AppRole; const state = generateCsrfToken(req.session.id); res.redirect(getAuthorizationUrl(role, state)); }); ``` ### Step 3: Token Storage with Role Metadata ```typescript interface StoredToken { accessToken: string; userId: string; role: AppRole; scopes: string[]; siteIds: string[]; // Which sites this token can access createdAt: Date; lastUsed: Date; } async function storeToken(token: StoredToken): Promise { // Encrypt token before storing const encrypted = encrypt(token.accessToken); await db.webflowTokens.upsert({ where: { userId: token.userId }, create: { ...token, accessToken: encrypted, }, update: { accessToken: encrypted, lastUsed: new Date(), }, }); } ``` ### Step 4: Per-Site Token Isolation Use site-scoped tokens to limit blast radius: ```typescript class SiteIsolatedClient { private clients = new Map(); // Each site gets its own token — compromise of one doesn't affect others registerSite(siteId: string, siteToken: string): void { this.clients.set(siteId, new WebflowClient({ accessToken: siteToken })); } getClient(siteId: string): WebflowClient { const client = this.clients.get(siteId); if (!client) { throw new Error(`No token registered for site ${siteId}`); } return client; } // Rotate a single site's token without affecting others rotateToken(siteId: string, newToken: string): void { this.clients.set(siteId, new WebflowClient({ accessToken: newToken })); console.log(`Token rotated for site ${siteId}`); } } const siteClients = new SiteIsolatedClient(); siteClients.registerSite("site-prod", process.env.WEBFLOW_TOKEN_PROD!); siteClients.registerSite("site-staging", process.env.WEBFLOW_TOKEN_STAGING!); ``` ### Step 5: Permission Check Middleware ```typescript import { Request, Response, NextFunction } from "express"; function requireWebflowScope(requiredScopes: string[]) { return async (req: Request, res: Response, next: NextFunction) => { const userToken = await db.webflowTokens.findByUserId(req.user.id); if (!userToken) { return res.status(401).json({ error: "No Webflow authorization" }); } // Check if user's token has the required scopes const missingScopes = requiredScopes.filter( s => !userToken.scopes.includes(s) ); if (missingScopes.length > 0) { return res.status(403).json({ error: "Insufficient Webflow permissions", missing: missingScopes, userRole: userToken.role, hint: `Role "${userToken.role}" does not include: ${missingScopes.join(", ")}`, }); } next(); }; } // Usage app.post( "/api/cms/items", requireWebflowScope(["cms:write"]), createItemHandler ); app.get( "/api/forms/submissions", requireWebflowScope(["forms:read"]), listSubmissionsHandler ); app.post( "/api/site/publish", requireWebflowScope(["sites:write"]), publishSiteHandler ); ``` ### Step 6: Audit Logging ```typescript interface WebflowAuditEntry { timestamp: Date; userId: string; role: AppRole; operation: string; siteId: string; resourceType: string; resourceId?: string; result: "success" | "denied" | "error"; scopes: string[]; ipAddress: string; } async function auditWebflowAccess(entry: WebflowAuditEntry): Promise { // Write to audit log (never delete audit entries) await db.auditLog.create({ data: { ...entry, timestamp: entry.timestamp.toISOString(), }, }); // Alert on suspicious patterns if (entry.result === "denied") { console.warn(`ACCESS DENIED: ${entry.userId} attempted ${entry.operation} on ${entry.resourceType}`); } // Alert on admin operations if (entry.role === AppRole.SiteAdmin && entry.operation.includes("publish")) { console.log(`ADMIN ACTION: ${entry.userId} published site ${entry.siteId}`); } } // Wrap API calls with audit logging async function auditedCall( entry: Omit, operation: () => Promise ): Promise { try { const result = await operation(); await auditWebflowAccess({ ...entry, timestamp: new Date(), result: "success" }); return result; } catch (error) { await auditWebflowAccess({ ...entry, timestamp: new Date(), result: "error" }); throw error; } } ``` ### Step 7: Token Rotation Schedule ```typescript async function checkTokenAge(): Promise { const tokens = await db.webflowTokens.findMany(); for (const token of tokens) { const ageInDays = (Date.now() - token.createdAt.getTime()) / (1000 * 60 * 60 * 24); if (ageInDays > 90) { console.warn( `Token for user ${token.userId} (${token.role}) is ${Math.floor(ageInDays)} days old. ` + `Rotation recommended.` ); // Send notification to user/admin } } } // Run weekly // cron: "0 9 * * 1" ``` ## Output - Role-to-scope mapping for Webflow OAuth - OAuth authorization with role-appropriate scopes - Per-site token isolation - Permission check middleware for API endpoints - Comprehensive audit logging - Token age monitoring and rotation alerts ## Error Handling | Issue | Cause | Solution | |-------|-------|----------| | OAuth scope mismatch | App requests more scopes than configured | Match app settings in Webflow dashboard | | 403 on API call | Token missing required scope | Re-authorize with correct role | | Token not found | User never completed OAuth flow | Redirect to authorization | | Audit gap | Error in async logging | Add fallback logging to console | ## Resources - [Webflow OAuth Reference](https://developers.webflow.com/data/reference/oauth-app) - [Webflow Scopes](https://developers.webflow.com/data/reference/scopes) - [Webflow Authentication](https://developers.webflow.com/data/reference/authentication) ## Next Steps For major migrations, see `webflow-migration-deep-dive`.