agentsclimarketplace

Webflow enterprise rbac

Skill jeremylongshore/claude-code-plugins-plus-skills/plugins/saas-packs/webflow-pack/skills/webflow-enterprise-rbac

425 plugins, 2,810 skills, 200 agents for Claude Code. Open-source marketplace at tonsofskills.com with the ccpi CLI package manager.

Install
npx -y skills add jeremylongshore/claude-code-plugins-plus-skills --skill webflow-enterprise-rbac

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

What its author says it does

Copied from the file, not written here

Configure Webflow enterprise access control \u2014 OAuth 2.0 app authorization, \ scope-based RBAC, per-site token isolation, workspace member management, and audit\ \ logging for compliance. Trigger with phrases like "webflow RBAC", "webflow\ \ enterprise", "webflow roles", "webflow permissions", "webflow OAuth scopes"\ , "webflow access control", "webflow workspace members".

The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.

SKILL.md

9.5 KB, as published. Nobody here has run it

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:

LevelMechanismGranularity
Workspace tokensAPI tokenAll sites in workspace
Site tokensAPI tokenSingle site
OAuth tokensOAuth 2.0 authorizationUser-authorized scopes per site

Scope-Based Permission Model

Webflow scopes map directly to API access:

ScopeRead AccessWrite Access
sites:readList/get sites
sites:writePublish sites
cms:readList collections, read items
cms:writeCreate/update/delete CMS items
pages:readList/get pages
pages:writeUpdate page settings
forms:readList forms, read submissions
ecommerce:readList products, orders, inventory
ecommerce:writeCreate products, fulfill orders
custom_code:readList registered scripts
custom_code:writeRegister/apply custom code

Instructions

Step 1: Define Application Roles

Map your application roles to Webflow scope sets:

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, string[]> = {
  [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:

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

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<void> {
  // 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:

class SiteIsolatedClient {
  private clients = new Map<string, WebflowClient>();

  // 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

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

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<void> {
  // 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<T>(
  entry: Omit<WebflowAuditEntry, "timestamp" | "result">,
  operation: () => Promise<T>
): Promise<T> {
  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

async function checkTokenAge(): Promise<void> {
  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

IssueCauseSolution
OAuth scope mismatchApp requests more scopes than configuredMatch app settings in Webflow dashboard
403 on API callToken missing required scopeRe-authorize with correct role
Token not foundUser never completed OAuth flowRedirect to authorization
Audit gapError in async loggingAdd fallback logging to console

Resources

Next Steps

For major migrations, see webflow-migration-deep-dive.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.