CORS requirements for Framer plugins

Framer plugins run on Framer-managed domains. When a plugin talks to an external API, that API must explicitly allow requests from those domains. Missing or incomplete CORS configuration is one of the most common reasons plugin submissions fail review.

This guide explains exactly which domains to allow and how to handle them safely.

Why CORS matters for plugins

Framer plugins run in the browser. Any request from a plugin to a backend API is subject to the browser’s CORS rules.

If the API does not allow the plugin’s origin, the browser blocks the request before it ever reaches the server. This usually shows up as a generic CORS error in the console, even though the backend itself is working correctly.

Plugin domain formats

Every Framer plugin is served from one of two domain patterns. Both must be allowed.

Production domain

  • id is a stable identifier for the plugin.

  • This domain is used by all end users once the plugin is approved.

  • This domain never includes a version ID.

Version-specific domain

  • versionId is unique for each submitted version.

  • This domain is used during plugin review and testing.

  • Only the plugin creator and the marketplace reviewer ever load this domain.

When a new version is approved, users are automatically switched to the production domain. If CORS is only configured for the version-specific domain, the approved plugin will break for users.

Common CORS mistakes

Allowing only one domain

Many APIs whitelist just one origin, usually the production domain. During review, the plugin runs on the version-specific domain and fails with a CORS error.

Allowing a single version ID

Some implementations hardcode a specific [id]-[versionId] origin. This works once, then breaks on the next submission because the version ID changes.

Using * with credentials

Using Access-Control-Allow-Origin: * does not work if the plugin sends cookies or authorization headers. Browsers block this combination.

The safest approach is to dynamically allow any origin that matches your plugin ID, and supports all Framer’s plugin domain patterns.

This avoids manual updates and works for all future versions without changes.

Example: dynamic CORS headers in JavaScript

The function below inspects the Origin header and returns the correct CORS headers if the request comes from a valid Framer plugin domain.

This pattern works in Node.js, Bun, or a Cloudflare Worker.

// Replace this with your plugin ID from the submission flow
const PLUGIN_ID = "yourPluginIdHere";

function getCorsHeaders(request) {
  const origin = request.headers.get("origin");

  if (!origin) {
    return {};
  }

  const framerPluginPattern = new RegExp(
    `^https://${PLUGIN_ID}(-[a-zA-Z0-9]+)?\\.plugins\\.framercdn\\.com$`
  );

  if (!framerPluginPattern.test(origin)) {
    return {};
  }

  return {
    "Access-Control-Allow-Origin": origin,
    "Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
    "Access-Control-Allow-Headers": "Content-Type, Authorization",
    "Access-Control-Allow-Credentials": "true",
  };
}
// Replace this with your plugin ID from the submission flow
const PLUGIN_ID = "yourPluginIdHere";

function getCorsHeaders(request) {
  const origin = request.headers.get("origin");

  if (!origin) {
    return {};
  }

  const framerPluginPattern = new RegExp(
    `^https://${PLUGIN_ID}(-[a-zA-Z0-9]+)?\\.plugins\\.framercdn\\.com$`
  );

  if (!framerPluginPattern.test(origin)) {
    return {};
  }

  return {
    "Access-Control-Allow-Origin": origin,
    "Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
    "Access-Control-Allow-Headers": "Content-Type, Authorization",
    "Access-Control-Allow-Credentials": "true",
  };
}
// Replace this with your plugin ID from the submission flow
const PLUGIN_ID = "yourPluginIdHere";

function getCorsHeaders(request) {
  const origin = request.headers.get("origin");

  if (!origin) {
    return {};
  }

  const framerPluginPattern = new RegExp(
    `^https://${PLUGIN_ID}(-[a-zA-Z0-9]+)?\\.plugins\\.framercdn\\.com$`
  );

  if (!framerPluginPattern.test(origin)) {
    return {};
  }

  return {
    "Access-Control-Allow-Origin": origin,
    "Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
    "Access-Control-Allow-Headers": "Content-Type, Authorization",
    "Access-Control-Allow-Credentials": "true",
  };
}

Handling preflight requests

For OPTIONS requests, return the same headers with a 204 or 200 response.

if (request.method === "OPTIONS") {
  return new Response(null, {
    status: 204,
    headers: getCorsHeaders(request),
  });
}
if (request.method === "OPTIONS") {
  return new Response(null, {
    status: 204,
    headers: getCorsHeaders(request),
  });
}
if (request.method === "OPTIONS") {
  return new Response(null, {
    status: 204,
    headers: getCorsHeaders(request),
  });
}

What to double-check before submitting

Before submitting a plugin for review, confirm the following:

  • Both domain patterns are allowed.

  • No version IDs are hardcoded.

  • Preflight requests return the correct headers.

  • Credentials are only enabled if they are actually required.

Getting CORS right once prevents review delays and avoids production-only failures after approval.