AxiomCode
AI 소프트웨어 엔지니어링을 위한 검증 가능한 인텔리전스, 형식화된 코드 그래프 기반
AI 코딩 에이전트를 하나의 보드에서 관리·프로젝트 메모리·품질 검사·안전 배포, 자체 인프라에서 로컬 제어
A local-first control plane that orchestrates AI coding agents on one shared board — project memory, quality checks and safe shipping, on your own infrastructure.
플러그인TypeScript · HTMLGitHub @mal4i6ka온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
AllCrew Figma Workspace: local-first design tokens, code export, MCP, and agent-driven Figma workflows
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
A local-first Figma development plugin and MCP workspace for Claude Code, Codex, Cursor, design tokens, project export, and controlled agent writes.
Design handoff is more than pixels. Agents need the variables behind values, component behavior, prototype edges, assets, and the live state of the file a designer has open. AllCrew exposes that context through a user-run loopback bridge, then keeps reads and writes behind separate switches visible in Figma.README 전문 보기 ↗A local-first Figma development plugin and MCP workspace for Claude Code, Codex, Cursor, design tokens, project export, and controlled agent writes — in Figma Design, Dev Mode, FigJam, Slides and Buzz.
Download the latest release · Install · Security model · Report an issue
Design handoff is more than pixels. Agents need the variables behind values, component behavior, prototype edges, assets, and the live state of the file a designer has open. AllCrew exposes that context through a user-run loopback bridge, then keeps reads and writes behind separate switches visible in Figma.
It also exports usable deliverables: mode-aware tokens, React, Django, Tauri, native color resources, documentation (also drawn on the canvas), and declarative custom plugin screens. The list is open-ended: an agent builds any other stack or format from the same framework-neutral reading of the design (design.ir, variables.get). People and agents working in the same file can leave each other messages that are stored in the file.
Core extraction and generation run inside Figma. Optional Delivery and Agent Listener connections go only to endpoints the user configures; AllCrew operates no hosted relay and collects no telemetry.
The two are complementary, and AllCrew is built to work alongside Figma's own server rather
than replace it. Figma's remote MCP is hosted, broad and OAuth-based. Beyond reading Design files,
almost everything it does in a file goes through one tool, use_figma, which runs whatever
Plugin API JavaScript the agent writes. AllCrew runs inside the file the designer has open. It
offers typed, schema-checked operations behind switches the designer can see, and it produces
repository-grade output.
Where AllCrew is ahead:
design.ir and variables.get, delivered to git, a pull request, a GitLab merge request or npm.Where Figma's MCP is ahead:
✅ a dedicated tool or op · 🟡 possible, but only by writing Plugin API JavaScript in use_figma,
or only partly · — not available
| Capability | Figma MCP | AllCrew |
|---|---|---|
| Read structure, layout, variables, motion | ✅ | ✅ |
| Read variable-font axes, shaders, vector regions | — | ✅ |
| Change layers: typed ops, one undo step each, behind Allow changes | 🟡 | ✅ |
| Create components, variants and styles | 🟡 | ✅ |
| Create and bind variables | 🟡 | ✅ |
| Search team libraries | ✅ | 🟡 uses Figma's search |
| Insert library components and styles by key | 🟡 | ✅ |
| Generate a design from code or HTML | ✅ | — |
| Generate images | ✅ | — |
| Code from a frame | ✅ React + Tailwind reference | ✅ packaged React, Django, Tauri, … any stack the agent builds from design.ir (SwiftUI, Compose, React Native) |
| Design tokens | — | ✅ CSS, JSON, TS, iOS, Android, … any format the agent writes from variables.get (values, modes, codeSyntax) |
DESIGN.md / COMPONENTS.md for agents | — | ✅ |
| Code Connect mappings | ✅ | ✅ templates and the official CLI |
| Code layers (Figma Make) | ✅ | 🟡 packs the export for Figma's importer |
| Markdown on the canvas | ✅ Specs (beta) | ✅ plain layers, refreshed on export |
| Version history: what changed and who | — | ✅ |
| Comments: read, post, reply |
| Capability | Figma MCP | AllCrew |
|---|---|---|
| Read the selection for code | ✅ desktop server | ✅ |
| Measurements: read and set | — | ✅ |
| Code in Dev Mode's code panel | 🟡 Code Connect snippets | ✅ React + CSS Modules |
| Capability | Figma MCP | AllCrew |
|---|---|---|
| Read a board | ✅ | ✅ |
| Generate a diagram | ✅ | — |
| Stickies, shapes with text, connectors | 🟡 | ✅ |
| Tables, code blocks, link previews | 🟡 | ✅ |
| Timer and who is on the board | 🟡 | ✅ |
| Capability | Figma MCP | AllCrew |
|---|---|---|
| Create a deck file | ✅ | — |
| Screenshots | ✅ | ✅ |
| Slides and rows, placed by position | 🟡 | ✅ |
| Transitions and skipped slides | 🟡 | ✅ |
| Slide grid: read and rearrange | 🟡 | ✅ |
| Capability | Figma MCP | AllCrew |
|---|---|---|
| Assets: create and set their type | — | ✅ |
| Text and media content: read and fill | — | ✅ |
| Smart resize | — | ✅ |
| Canvas grid: read and rearrange | — | ✅ |
| Capability | Figma MCP | AllCrew |
|---|---|---|
| No account sign-in or per-seat rate limits | — | ✅ |
| Works on any plan | 🟡 | ✅ |
| Separate read and write switches the designer sees | — | ✅ |
| Operation log of what the agent did | — | ✅ |
| Messages between collaborators and their agents, stored in the file | — | ✅ |
| Delivery to a folder, git, a pull request, a GitLab merge request or npm | — | ✅ |
| Design-to-site fidelity check | — | ✅ |
| Custom screens inside the plugin | 🟡 generative plugins | ✅ AllCrew SDK modules, … any screen an agent writes and installs (modules.schema, modules.install) |
| Make, shaders, Weave | ✅ | — |
AllCrew also relays Figma's desktop Dev Mode MCP server through the bridge (figma.mcp), so an
agent connected only to AllCrew can still call get_design_context or get_metadata when Figma
Desktop runs that server and the seat includes Dev Mode.
Official references: Figma MCP server, tools and limits.
allcrew-figma-workspace-v<version>.zip and SHA256SUMS from the
latest release.shasum -a 256 -c SHA256SUMS
manifest.json in the extracted folder.Figma's browser app cannot import development plugins. Updating means downloading the new release, replacing the extracted directory, and restarting the plugin.
npm ci
npm run build
Import this checkout's manifest.json. npm run dev rebuilds on change; npm run package
creates the release archive and refuses to package stale bundles.
figma.clientStorage and
re-apply on every run.Drag the bottom-right corner to resize the plugin between 340×320 and 1240×1200 pixels.
Double-click the handle to fit the current page's height without changing its width. The size is
stored per machine in figma.clientStorage and restored on the next run. Every native view
consumes the available width; at narrow sizes the header, settings controls, palette editor,
module actions, tables and footer collapse or scroll without clipping. At 560 pixels wide the
palette editor gains a third field column. At 720 pixels the export, token, DS Tools and Agent
Listener screens switch to semantic split grids. Django and Tauri keep setup and review work in
independent vertical lanes, so a tall linter cannot push a wide regeneration card below an empty
grid row. Dense palette, remap and video workflows retain wider spans, while declarative module
blocks use an auto-fit grid. Heterogeneous screens avoid row-spanning cards, so tall skill/code
content cannot create a hole beneath a shorter sibling.
The internal workspace stops growing at 1200 pixels, Agent skill content is capped at 520 pixels,
and lone DS generator cards stop at 560 pixels in split layouts. Resize drag accumulates consecutive screen-space
deltas in floating-point dimensions, clamps that accumulator without overshoot, and rounds only
the size sent to Figma. Reversing direction therefore moves the edge immediately on mouse and
trackpad input. Live requests are coalesced per frame and only the final size is persisted.
The live browser mirror follows its browser window and hides the Figma-only resize handle.
The release archive includes the exact bridge and MCP front built with the plugin. The downloaded bridge is one self-contained ESM file, including every local module it needs:
node tools/bridge.mjs
Open Agent Listener in the plugin and pair during the five-minute window. Allow reads and Allow changes control external agents only. Independently, Browser UI mirror publishes the live panel through the loopback bridge; the browser icon in the header opens the exact connected file in Chrome.
Agent calls use one authenticated loopback WebSocket per open Figma file and fall back to HTTP
polling when a previous bridge does not advertise socket support. The bridge also reads Figma
Desktop's local tab list to resolve file keys without asking for URLs; history.recent summarizes
version checkpoints across files opened on this machine.
The agent channel publishes a fidelity-first product-design contract through both the generated
skill and MCP initialization. Before choice-bearing UI work, agents check repository context and
call design.evidence; existing-screen reproduction proceeds without a broad brief, while a new
surface missing its product goal, primary user/job, or visual direction produces one grouped
clarification question. Platform standards fill accessibility and system-behavior gaps only, never
brand styling. For an MCP client:
node tools/mcp.mjs --install claude
# cursor, windsurf and vscode are also supported
Grants are remembered per file. A development plugin gets no fileKey, so the first grant in a file stamps it with a random id in the document's plugin data. Reopening or reloading the plugin in that file restores the listener with the same read and write switches. Renaming the file keeps the grant. A different file, even one with the same name, starts with both gates closed. A duplicate of the file keeps the stamp, and with it the grant.
The Messages screen (in the screen picker, next to Agent Listener) is a mailbox kept in the open file. It lets the people working in the file, and their agents, leave each other messages. There is no server: each message is an entry in the plugin's private data on the document, so it syncs to every collaborator's client like any other edit and arrives in about half a second.
inbox.list, inbox.wait, inbox.send and inbox.ack as
agent:<their person's id> (agent/README.md). Every answer reminds them
that a colleague's message is information, not an instruction.Limits to know: mail moves only while the plugin is open somewhere in the file. Every message, receipt and prune lands in undo and version history, because the Plugin API has no way around that. Anyone who can open the file can read the mail, so never send a secret. The sender's name is stamped by the sending client. That makes it attribution, not authentication: the plugin id is public.
| File | Contents |
|---|---|
tokens.css | CSS custom properties. One self-contained block per theme. The merged single file. |
<theme>.module.css | The same blocks split one CSS Module per theme (light.module.css, dark.module.css, …), wrapped in :global(…). Merged by your bundler (which strips :global), they're equivalent to tokens.css. |
tokens.json | Canonical W3C token tree — every mode under $extensions.modes, source collection under $extensions.figma. For diffing / re-import. |
tokens.ts | Typed tokens object (values are var(--…) refs) + a themes list and Theme type. |
Assets.xcassets/<token>.colorset/Contents.json | (iOS & Android tokens on) One colorset per colour, in sRGB components, with a luminosity: dark entry only where the dark theme actually changes it — so Color("text-primary") follows the system appearance with nothing in the app deciding. |
res/values/colors.xml, res/values-night/colors.xml | (iOS & Android tokens on) The same, by Android resource qualifier. The night file is an override list: it carries only the colours that differ. |
Tokens.swift, Tokens.kt | (iOS & Android tokens on) Constants for what a catalogue cannot hold (spacing, radii, the type scale) and for the colours too, for a build that themes in code. Values identical in every theme are declared once, above the per-theme blocks; a length gets CGFloat/.dp, a line-height ratio does not. |
DESIGN.md | The design system written for people and agents: tokens grouped by role (roles come from Collections in the export), the full token reference, typography, themes, breakpoints, components, and a prompt for agents. |
COMPONENTS.md | (Component contracts on) One section per component: its Description as a behaviour contract, its properties and its documentation links, with an index. |
The Export Settings page (open it from the gear in the header) isolates the opinionated parts of the output so different consumers can keep their own preferences (the choices persist per-user):
| Setting | Default | What it does |
|---|---|---|
| Inline primitives | on | Resolves aliases that point at a primitive (raw, single-mode value) into the literal, and drops the primitive layer. Aliases between semantic tokens stay as var(--…). |
| Flatten all aliases | off | With Inline primitives on, also resolves the semantic→semantic var(--…) refs into literals, so no references remain in the output. Off keeps the readable, themeable semantic layer. |
| Theme attribute | data-theme-name | The attribute the theme blocks key off ([<attr>="Dark"]). Set it to data-theme to match the AllCrew Figma Workspace board, or anything else. |
| Theme collections table | empty | Maps any number of free-plan single-mode collections into exported multi-theme structures. Each row names the output structure, source collection and exported theme; repeat a structure name for Light, Dark, High contrast or additional brand themes. Conventional theme + theme-dark and theme-light + theme-dark layouts are detected automatically. |
Per-theme .module.css | on | Whether to emit the per-theme module files alongside tokens.css. |
CSS Modules :global() | on | Wrap module-file selectors in :global(…) (valid CSS Modules) or leave them plain. |
| iOS & Android tokens | off | Also emit the asset catalogue, res/values{,-night}/colors.xml, Tokens.swift and Tokens.kt. Off by default because a palette becomes one colorset directory per colour, which is noise in a package a web project unzips. |
| Collections in the export | Export | One row per variable collection of the open file, with a role picked from a list: Export, Hide from code (a primitive layer stays out while its values still reach the tokens that alias it), Breakpoints → --bp-*, (a parallel product collection gets its own stylesheet built from the shared base plus that collection), or a DESIGN.md role (spacing, radius, text colour, motion duration, …). Saved roles for collections the file does not have stay listed, struck through. |
Themes are the modes of collections that have more than one. With none, the
stylesheet is a single :root. The default theme occupies :root, so an
un-themed page still renders; every other theme block holds only the values it
changes.
Before anything is emitted the export is shaped for code:
_ or . (Figma's
spelling for "not for consumers") are left out, while anything aliasing them
still resolves;--bp-* come only from threshold variables (breakpoint/<name>, bp/…,
min-width/…), narrowest first — never from frame widths;TIMING variables export as milliseconds, EASING variables as their
cubic-bezier() (or a sampled linear() for a spring), and a composed
colour ({ color, opacity }) as the flat colour it paints in each mode;oklch(), oklab() or
color(display-p3 …). Figma itself drops the authored space from a solid fill
or stroke whose opacity is below 100% (gradient stops keep it), so such a paint
exports as its sRGB projection.With Inline primitives on (the default), the raw scale is collapsed into the tokens that use it — no primitive variables in the output:
:root,
[data-theme-name="Light"] {
/* Tokens */
--colors-action-default: #002780;
--spacing-md: 8px;
}
/* add a "Dark" mode to the collection in Figma → a second block appears */
[data-theme-name="Dark"] {
/* …only the variables whose dark value differs… */
}
Turn it off to keep the primitive layer and reference it via aliases instead (byte-identical to the AllCrew Figma Workspace board, modulo the attribute name):
:root,
[data-theme-name="Light"] {
/* Primitives */
--colors-b-800: #002780;
--sizes-1: 8px;
/* Tokens */
--colors-action-default: var(--colors-b-800);
--spacing-md: var(--sizes-1);
}
A primitive used directly in code (not via an alias) can't be detected by the plugin — it will be inlined away. Turn Inline primitives off if you rely on the raw scale at call sites.
Switch theme by setting the attribute on any ancestor:
<html data-theme-name="Dark">
<theme>.module.cssThe same theme blocks are also emitted one file per theme, named after the mode
(light.module.css, dark.module.css, … — slugified; default.module.css when
the export is a single :root), wrapped in :global(…) so they're valid in a
CSS-Modules project (Next.js, etc.). A collection marked Separate file gets
its own <name>.css, and its modules are prefixed with it
(business-dark.module.css); the export summary names the tokens one product
file defines that another lacks.
/* light.module.css */
:global(:root),
:global([data-theme-name="Light"]) {
--colors-action-default: var(--colors-b-800);
--spacing-md: var(--sizes-1);
}
The default theme also lands on :global(:root). Concatenating every
*.module.css (or letting your bundler merge the ones you import) reproduces
tokens.css — that's the "merged single file". For runtime theme-switching,
keep every theme present (use tokens.css, or import all *.module.css) and
toggle data-theme-name.
With the Django × Bootstrap preset the package is not a folder of templates waiting for someone to write a URLconf — it is a site you can run:
unzip export.zip -d site && cd site
pip install -r requirements.txt
DJANGO_DEBUG=1 python manage.py runserver
| What ships | Where it comes from |
|---|---|
manage.py, config/{settings,urls,wsgi,asgi}.py, design/{views,urls,pages,context_processors}.py | Settings → Target → Django project (off when you apply onto a project that already has its own) |
templates/base.html + one template per frame + one partial per component | the frames in scope; a component set becomes one partial, its variants CSS modifiers |
static/css/project.css, tokens.css, interactions.css, transitions.css | layout/paint, design tokens, reaction states, prototype page transitions |
locale/figma.po + locale/<code>/LC_MESSAGES/django.po | Settings → i18n → Target languages |
design/pages.py is the generated page registry — the one file a re-export
overwrites. Everything else reads it: the prototype's start frame serves at /,
every other page at /<slug>/, and the navMap context processor reverses those
routes so the {{ navMap.nav_… }} hrefs the markup already emits resolve (and pick
up the active locale prefix). Configure Target languages and the routes move
under i18n_patterns with a no-JS language switcher in the shell.
Markup. A frame named Header/Nav/Footer/Sidebar becomes that element, a page root's
other direct children become <section>, a text layer whose name states a level (Heading 2,
Title/H3) becomes <h2>/<h3> — with exactly one <h1> per page — and a layer that only reacts
to a click becomes a real <button type="button"> instead of a div no keyboard can reach. Figma's
own auto-names (Rectangle 12, Vector) render as alt=""/aria-hidden, because a screen reader
announcing "Rectangle 12" is worse than silence. A layer the designer pinned with Fixed position
when scrolling gets position: fixed/sticky — that choice used to be read and then dropped.
What the CSS pass could only approximate or could not draw at all (squircle corners, NOISE /
TEXTURE / GLASS / SHADER effects and fills) is listed per node in export-report.json under
fidelityNotes, instead of disappearing into a console.
Motion. Reaction states (ON_HOVER/ON_PRESS/ON_CLICK → CHANGE_TO) are
diffed against the destination variant including its descendants, so a hover that
recolours an icon, re-pads a button or reveals a badge survives. Overlays render the
destination frame's own markup into a <dialog> with an open/close animation.
Page-to-page prototype transitions (DISSOLVE / PUSH / MOVE / SLIDE / SMART_ANIMATE)
become cross-document view transitions — Smart Animate assigns a shared
view-transition-name to the layers matched across both screens. Everything is
wrapped in prefers-reduced-motion guards.
The Smart Animate diff behind those states covers what Figma covers: position, size, rotation,
opacity, corner radii, strokes, the whole fill stack (gradients included, marked non-interpolable
where CSS cannot tween them), shadows and blurs, text size/weight/spacing/colour, auto-layout
padding and gap, visibility (with transition-behavior: allow-discrete) and blend mode — on the
changed layer itself as well as its descendants. A state that swaps a photo ships the
destination variant's image with the page assets and references it; when that image cannot be
exported the declaration is dropped rather than emitted as the url(<path-to-image>) placeholder
Figma hands out, which resolves to a 404 and blanks the element on hover.
Springs. A CUSTOM_SPRING reaction is solved from its own physics (ω₀ = √(k/m),
ζ = c / 2√(km), plus the designer's initialVelocity), and a named preset (GENTLE, BOUNCY, …)
from its bounce at the frequency whose period is the duration typed next to it. The emitted
linear() curve runs for the spring's real settle time, so a 150 ms bouncy press is a 150 ms-ish
bounce rather than the same half-second curve every spring used to collapse into.
Figma publishes no physics for its named presets, so those bounces come from a reverse-engineered
table — the one estimate left in the emitted motion, and export-report.json says so:
estimatedSpringPresets counts the curves that rest on it, and springPresetCalibration reports
what the file's own springs say the table should hold whenever Figma does supply the numbers. A
preset that arrives with real parameters is solved from them and never touches the table at all.
Adaptivity. Frames named Home / desktop + Home / mobile collapse into one
page: the widest frame is the DOM, the narrower ones become @media blocks, and
nodes that exist only in a narrow frame (the burger, a stacked CTA) are spliced in
hidden and revealed at their breakpoint. base.html carries the viewport meta, so
those queries apply on a real phone.
The React target writes a component per Figma component set, and beside each one a
<Component>.figma.ts — a Code Connect template that tells Figma this is the code for that
design component. From then on Dev Mode and Figma's official MCP answer with
<Button type="Primary" …> and a real import, instead of anonymous markup a developer has to
recognise. Code Connect 2 accepts only template files (the figma.connect(...) parser files of
1.x are refused by its CLI), and the export writes nothing else:
figma.config.json (include: src/components/**/*.figma.ts, label React,
language jsx);@figma/code-connect ^2.0.2 in devDependencies, with npm run figma:check (templates typed
against Code Connect's own figma-types in tsconfig.figma.json, kept out of the app build)
and npm run figma:publish.Props are not guessed: the same variant/text/boolean definitions the component's own union types
come from are read with getEnum / getString / getBoolean on figma.selectedInstance, with
the Figma property names verbatim — Button-lable and Meduim are mapped as spelled, because that
is what the file contains — and rendered through figma.helpers.react.renderProp. Each template's
id is derived from the node id, so renaming a component or re-exporting keeps the published
mapping.
The file key. A template addresses its component by node URL, and the URL needs the file's
key, which only a private Organization plugin can read (figma.fileKey). Everywhere else pass the
file's Figma link as fileUrl to EMIT_REACT; through the bridge it is filled in automatically
from Figma Desktop's open tab when the bridge can match one. Without a key no templates are
written at all and the export report says how to provide one — a mapping pointing at a guessed
key would map nothing.
Publishing from an agent. The bridge runs the pinned official CLI on the repository
directory — codeconnect.parse and codeconnect.map offline, codeconnect.preview and
codeconnect.publish (a dry run unless dryRun: false, which needs the plugin's write gate) with
a token. See the Code Connect ops in agent/README.md.
Two things worth knowing before troubleshooting a publish:
?node-id=819-95512 — a dash, not the colon Figma stores), so publishStatus: UNPUBLISHED
publishes fine. Publishing only matters for the MCP route, where add_code_connect_map refuses
with "Published component not found".FIGMA_ACCESS_TOKEN; the bridge passes it the same
token it uses for REST ($FIGMA_TOKEN, else ~/.allcrew-channel/figma-token).npm run fidelity compares Figma frames with the live (or local) pages built from them, matching
by text rather than by markup: wrong type styles, displaced or re-wrapped text, sections that
push everything below them, design text baked into images, and content missing from the page.
It reads the design over REST (cached per file version) and shoots the page in system Chrome, so
Figma does not need to be open. npm run fidelity:deliver turns a run into ticket drafts that
carry the design's own spec, Figma comments pinned per section, and a CSS fix brief. Setup,
findings and the project config format: docs/fidelity.md.
Reading variable values over REST is Enterprise-only, so on Organization the plugin itself is the extractor — but everything around it is automated. The designer publishing the library is the trigger; the plugin ships the package; a small receiver does the git/npm/folder work.
Plugin side (Export Settings → Delivery):
server/receiver.mjs, so the copy they get can never be a different version.x-allcrew-channel-secret; must match the receiver. (This is the
only secret in the plugin — git/npm credentials never leave the receiver.) Pair fills
both fields from a receiver running on this machine; see Secrets below.folder / git (commit + push) / pr (branch + gh pr create) / mr
(branch + GitLab merge request) / npm (publish), plus the non-secret route (repo /
branch / base / project / path / package). Only the fields the chosen target uses are shown.Receiver side (server/receiver.mjs) — a dependency-free Node script you run on any
host; it executes the target using the host's own git / gh / npm auth:
ALLCREW_CHANNEL_FOLDER_BASE=/abs/path/for/folder/target \
node receiver.mjs # listens on :8787 (override with PORT)
Nothing is baked into the build and nothing is distributed. A receiver with no
ALLCREW_CHANNEL_SECRET mints its own into ~/.allcrew-channel/receiver-secret (0600) and opens a
five-minute pairing window; the plugin's Pair button collects it, and the first pair
closes the window. Ten designers means ten different secrets, none of which anyone had to
send anyone. Rotate by deleting the file and restarting.
Pairing is unauthenticated by design, bounded three ways: loopback only, five minutes from a start someone typed by hand, and closed by the first success. It grants nothing a local process could not get by reading the same file.
For a shared or remote host, set ALLCREW_CHANNEL_SECRET yourself — the receiver uses it and opens
no pairing window (--pair forces one), and you type the same value into the plugin.
Never bake one secret into the plugin for everyone: it is a key to every teammate's host
that cannot be rotated without a rebuild.
The plugin only ever HTTP-POSTs, so the receiver is portable: point the plugin's endpoint
at wherever it runs (localhost, a Tailscale host, CI). The contract is one JSON POST of
{ target, dryRun, route, files }, where each file is UTF-8 text or { "base64": "…" }
for bytes; the receiver verifies the secret, checks every path and value before writing
anything, runs the target and returns { ok, target, dryRun, detail, plan, url }. See
server/README.md for the full contract and the branch guards (git
refuses the default branch; pr/mr refuse an existing, protected or base branch).
Fully hands-off (no designer either) would need the Variables REST API = Enterprise. The transform is already shareable, so that upgrade is a small step if you ever take it.
The receiver uses only Node.js built-ins — no npm install. The host needs:
| Target | Required on the host |
|---|---|
folder | Node.js only |
git | git + SSH key / credential helper for the repo |
pr | git + gh CLI authenticated (gh auth login) |
mr | git + ALLCREW_CHANNEL_GITLAB_TOKEN (API scope) |
npm | node + npm login (or ~/.npmrc with token) |
| Variable | Required | Default | Description |
|---|---|---|---|
ALLCREW_CHANNEL_SECRET | no | minted | Set it to manage the secret by hand; suppresses pairing |
ALLCREW_CHANNEL_SECRET_FILE | no | ~/.allcrew-channel/receiver-secret | Where a minted secret is stored |
PORT | no | 8787 | HTTP port |
ALLCREW_CHANNEL_FOLDER_BASE | for folder | — | Absolute base dir; route.path resolves under it |
ALLCREW_CHANNEL_WORK_DIR | no | <tmp>/allcrew-channel-tokens | Scratch dir for git clones and dry-run plans |
ALLCREW_CHANNEL_MAX_BODY_MB | no | 64 | Largest accepted delivery body |
ALLCREW_CHANNEL_GITLAB_TOKEN | for mr | — | GitLab token used to open merge requests and read protected-branch rules |
ALLCREW_CHANNEL_GITLAB_API | no | https://gitlab.com/api/v4 | GitLab API root for self-hosted instances |
ALLCREW_CHANNEL_GIT_AUTHOR_NAME / _EMAIL | no | AllCrew defaults | Commit identity |
ALLCREW_CHANNEL_ALLOW_DEFAULT_BRANCH_PUSH | no | off | 1 lets the git target push to main/master/the remote default branch |
The simplest setup — run the receiver locally, expose it to other Figma sessions via Tailscale:
# .env (keep out of git)
ALLCREW_CHANNEL_SECRET=some-random-string
ALLCREW_CHANNEL_FOLDER_BASE=/Users/you/projects/design-tokens/src
# run
ALLCREW_CHANNEL_SECRET=some-random-string \
ALLCREW_CHANNEL_FOLDER_BASE=/Users/you/projects/design-tokens/src \
node server/receiver.mjs
Plugin endpoint: http://<tailscale-hostname>:8787
npm install -g pm2
# create ecosystem file — .cjs, since this repo is "type": "module"
# (do NOT commit — contains the secret)
cat > ecosystem.config.cjs <<'EOF'
module.exports = {
apps: [{
name: "allcrew-channel-receiver",
script: "server/receiver.mjs",
env: {
ALLCREW_CHANNEL_SECRET: "your-secret-here",
ALLCREW_CHANNEL_FOLDER_BASE: "/srv/tokens",
PORT: "8787"
}
}]
}
EOF
pm2 start ecosystem.config.cjs
pm2 save && pm2 startup # survive reboots
# /etc/systemd/system/allcrew-channel-receiver.service
[Unit]
Description=AllCrew Figma Workspace receiver
After=network.target
[Service]
ExecStart=/usr/bin/node /opt/allcrew-channel/server/receiver.mjs
Restart=on-failure
Environment=PORT=8787
Environment=ALLCREW_CHANNEL_SECRET=your-secret-here
Environment=ALLCREW_CHANNEL_FOLDER_BASE=/srv/tokens
WorkingDirectory=/opt/allcrew-channel
[Install]
WantedBy=multi-user.target
systemctl enable --now allcrew-channel-receiver
FROM node:20-alpine
RUN apk add --no-cache git github-cli npm
WORKDIR /app
COPY server/receiver.mjs ./
EXPOSE 8787
CMD ["node", "receiver.mjs"]
docker build -t allcrew-channel-receiver .
docker run -d \
-p 8787:8787 \
-e ALLCREW_CHANNEL_SECRET=your-secret \
-e ALLCREW_CHANNEL_FOLDER_BASE=/tokens \
-v /host/tokens:/tokens \
allcrew-channel-receiver
The receiver itself is a push-side HTTP server, not a pull-side CI job. But you can use the git or pr target and let the receiver commit into the repo — Actions will then pick it up on push.
Designer triggers delivery in Figma
→ receiver commits tokens to `tokens` branch
→ Actions workflow runs on push to that branch
→ (optional) opens PR into main, runs style-dictionary, etc.)
Example Actions workflow for the downstream step:
name: Sync design tokens
on:
push:
branches: [tokens]
paths: ["tokens/**"]
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npx style-dictionary build # or whatever your pipeline is
- run: |
git config user.name "tokens-bot"
git config user.email "[email protected]"
git add -A && git diff --cached --quiet || git commit -m "chore: rebuild tokens"
git push
The receiver inherits credentials from the host environment — never from the plugin or the POST body.
| Target | How to auth |
|---|---|
git / pr | SSH key in ~/.ssh configured for the repo host (GitHub, GitLab, etc.) — or HTTPS with a credential helper (gh auth setup-git) |
pr | gh auth login — gh CLI must be authenticated |
npm | npm login or NPM_TOKEN in ~/.npmrc: //registry.npmjs.org/:_authToken=${NPM_TOKEN} |
curl http://localhost:8787
# {"ok":true,"service":"allcrew-channel-tokens-receiver","targets":["folder","git","pr","mr","npm"],"capabilities":{…}}
| Symptom | Likely cause |
|---|---|
401 bad or missing secret | Secret in plugin doesn't match ALLCREW_CHANNEL_SECRET |
git push fails | Host's git auth not set up for that remote |
gh pr create fails | gh auth login not done on the receiver host |
folder target needs ALLCREW_CHANNEL_FOLDER_BASE | Env var not set |
| No changes committed | Tokens were already up-to-date (not an error) |
Mode 1) is not
treated as a theme; its values fold into every theme block.ui.html.
manifest.json does declare networkAccess.allowedDomains: ["*"], and it has to: two
optional features dial out and neither host can be known ahead of time — the delivery step
POSTs the package to a receiver the user configures, and the agent listener talks to a loopback
bridge on a port the designer chooses. allowedDomains takes whole URLs and a port cannot be
wildcarded, so a narrower list would be wrong for half of any team. Nothing in the extraction
path uses it.editorType: ["figma", "dev", "figjam", "slides", "buzz"], capabilities: ["inspect", "codegen"],
permissions currentuser, teamlibrary, activeusers, fileusers). In Dev Mode it also
answers the code panel with the React export. What each editor's Plugin API offers is compared
in docs/editors.md. It reads variables
through the in-editor Plugin API, so no Enterprise / Variables REST API is
required — it works on any plan, including Organization. Dev Mode itself is gated
by Figma to a Dev or Full seat (no manifest setting bypasses that); anyone with
a normal editor seat can still run it in Design mode with no extra cost.[data-theme-name="…"] (per spec) but is configurable
in Export settings — set it to data-theme to match the AllCrew Figma Workspace board.tokens.json is always the full, un-inlined tree regardless of the Inline
primitives setting, so it stays lossless for diffing / re-import.127.0.0.1 by default. Do not expose it on a public interface.~/.allcrew-channel/agent-secret, receiver secrets, and Figma tokens as credentials.See SECURITY.md for the trust model and private vulnerability reporting.
Data handling: PRIVACY.md.
MIT © 2026 Alexander Lugachev.
The transform in code.js is a dependency-free port of the AllCrew Figma Workspace board's
src/lib/figma.ts (variablesToW3CMultiMode) + src/lib/design-system/tokens-transform.ts.
With Inline primitives off and the theme attribute set to data-theme, the
output is byte-identical to the board. The export options layer on top of that core
transform; keep the core in sync with the board if the token format changes.
Files:
manifest.json plugin manifest
dist/code.js sandbox and Codegen runtime
dist/ui.html plugin panel
agent/ local bridge, MCP front, and channel documentation
server/ optional Delivery receiver
| — |
| ✅ |
ICONS.md(Component contracts on) Icon components, which carry search keywords rather than behaviour, listed compactly instead of crowding COMPONENTS.md. |
previews/*.png | (Component previews on) A picture of every main component, up to the preview budget. |
README.md | Auto-generated usage notes for that specific export. |
name.css| Include library variables | off | Also export the enabled libraries' own collections. Not needed for values this file takes from a library: aliases into a library are resolved by id and substituted without it. It is a network read per token, and the panel reports progress while it runs. |
| Component contracts | on | Write COMPONENTS.md (and ICONS.md) from each component's Description and documentation link. |
| Component previews | off | Render a PNG of every main component into previews/. Slower scan, bigger package. |
| Preview budget, MB | 8 | Images land in the repository; components past the budget ship without a picture. |
| On the canvas | off | Every export also lays the package's Markdown out on a Docs page (FigJam: the current board), one frame per document: headings, native lists, tables, code, quotes and previews as ordinary layers. Index entries and links between documents jump to their frame. A re-export refreshes the same frames in place, so edits made there are replaced. Agents can do the same with DOCS_TO_CANVAS. |
| Automatic update discovery | off | Checks the latest GitHub Release at most once every 24 hours. A manual Check now action is always available. When a newer version exists, the plugin shows a quiet header banner with release and direct-download actions. |
| Browser UI mirror | off | Publishes the exact live plugin panel through the paired loopback bridge, independently from Agent Listener read/write permissions. While enabled, the header browser icon opens the connected file in Chrome. |