Penpot

Keep a Penpot file’s design tokens and a Brand’s token sets in step, both ways. Stoaken shows what would change on each side before anything is written; you choose the direction.

Set up

  1. In Penpot, open the file (any file; an empty one is fine) and copy its address from the browser. It looks like https://design.penpot.app/#/workspace?team-id=…&file-id=….
  2. Make an access token: your account menu, Your account, Access tokens, Generate new token. It acts as you, so use an account that can edit the file. Note the expiry date if you set one.
  3. In Stoaken, open Settings > Penpot, pick the Brand, paste the link and the token, and Connect. You need Editor access on the Brand. The token is stored encrypted and never shown again.
  4. The Sync panel checks the file straight away and shows both directions. Send fills an empty file with the Brand’s tokens and themes.
Design tokens arrived in Penpot 2.x. The sync is tested end to end against a real Penpot 2.18.4.

How it pairs

StoakenPenpotMatched by
A token setA token setName
A token (its path, like color.brand.primary)A token of the same nameName
A theme and its active setsA theme in the “Stoaken” group, with those sets switched onName
An alias, like {color.brand.primary}A reference, written the same wayKept as a reference, never flattened

Penpot sets with no Stoaken set of the same name are Penpot’s own: Send never touches them, and Bring offers each as a new set. Themes in other Penpot groups are left alone. Penpot’s hidden internal theme is ignored.

Send and Bring

The two columns describe the same differences from opposite sides, so choose which side is right.

Send to PenpotBring from Penpot
MakesEach paired Penpot set match Stoaken: adds, updates and removes tokensEach set you choose match Penpot: adds, updates and removes tokens
Leaves alonePenpot’s own sets; tokens Stoaken can’t hold (text case, for instance)Tokens Penpot can’t hold (motion, for instance)
ThemesCreates or updates the “Stoaken” themesNot brought; themes are made in Stoaken. A new set joins the Brand’s first theme
Goes liveAt once, in the Penpot fileNever on its own: changes land in the working sets, and ship when a release is approved

Both directions compare tokens in Penpot’s form, so 16 and 16px, or #007C2B and #007c2b, don’t count as changes. If someone saves the Penpot file in the moment between reading and writing, Stoaken reads it again and retries once.

With every release

Stoaken is the source of truth; Penpot gets what’s been approved. When a release is approved, its rollout window has a Penpot step (picked for you once a file is connected). It checks that the Brand’s token sets are exactly that live release, then sends it in one click:

The step findsWhat happens
The sets match the releaseIt shows what Penpot would take, and sends it.
The sets have changed since the releaseIt won’t send: release those changes first (or undo them), so Penpot never gets unapproved tokens.
Tokens that differ in Penpot but aren’t in the releaseIt names them before sending puts Stoaken’s version back. If a designer meant them, bring them into Stoaken and release them instead.
A newer release is liveIt points you at that release’s rollout.

Sending from Settings > Penpot sends the working sets as they are, drafts included; the page says when they hold changes no release carries yet.

What converts

Stoaken (DTCG)PenpotNotes
colorcolorsRGB: #rrggbb, or rgba() with transparency. Other colour spaces stay in Stoaken.
dimensionone of fontSizes, letterSpacing, borderRadius, borderWidth, spacing, sizing, dimensionChosen from the token’s path (radius.md is a border radius). px are written as plain numbers; rem as 1.5rem.
numberopacity, rotation or numberBy path: opacity.disabled is an opacity.
fontFamilyfontFamiliesA list of families.
fontWeightfontWeights400, or a named weight.
typographytypographyFamily, size, weight, line height and letter spacing.
shadowshadowOne or more layers, inset included.

Penpot’s types are finer than DTCG’s. When a token comes from Penpot with a type its name wouldn’t give back (a font size called heading.xl), Bring records it so Send returns the same type:

A token brought from Penpot
"heading": {
  "xl": {
    "$type": "dimension",
    "$value": { "value": 40, "unit": "px" },
    "$extensions": { "app.penpot": { "type": "fontSizes" } }
  }
}

What stays behind

Each column lists what can’t cross, and why, under the set’s details.

Stays in StoakenStays in Penpot
duration, cubicBezier, strokeStyle, border, transition, gradient: Penpot has no tokens for themtextCase, textDecoration, string, boolean, other: DTCG has no type for them
Colours outside sRGB; dimensions in units other than px and remValues DTCG can’t hold: percentages, and formulas like {spacing.md} * 2

Whatever stays behind is protected: Send doesn’t remove Penpot’s, and Bring doesn’t remove Stoaken’s.

Self-hosted Penpot

Paste your own Penpot’s file link; the address is taken from it. Stoaken calls Penpot from its servers, so the address must be reachable from the internet over https; private and local addresses are refused. Your Penpot needs access tokens turned on (the enable-access-tokens flag).

To try the sync against a Penpot of your own, the repository’s end-to-end test starts one in Docker:

Terminal
scripts/penpot-local.sh            # start Penpot, run the sync test
PENPOT_VERSION=2.20.0 scripts/penpot-local.sh
scripts/penpot-local.sh down       # stop it and delete its data

Troubleshooting

IfWhat to do
Penpot refuses the access tokenIt has expired or was deleted. Make a new one and reconnect.
The file isn’t found, or access is refusedCheck the link, and that the token’s account can open the file.
The address can’t be usedSelf-hosted: it must be public and https.
A set shows “New in Penpot”Penpot has no set of that name yet; Send creates it.
Tokens listed as “can’t go to Penpot”Expected for types Penpot doesn’t have; they stay in Stoaken.
You bring a set Stoaken doesn’t haveIt’s created and joins the Brand’s first theme (a Default theme if there’s none), like a set added on Overview.