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
- 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=…. - 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.
- 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.
- The Sync panel checks the file straight away and shows both directions. Send fills an empty file with the Brand’s tokens and themes.
How it pairs
| Stoaken | Penpot | Matched by |
|---|---|---|
| A token set | A token set | Name |
| A token (its path, like color.brand.primary) | A token of the same name | Name |
| A theme and its active sets | A theme in the “Stoaken” group, with those sets switched on | Name |
| An alias, like {color.brand.primary} | A reference, written the same way | Kept 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 Penpot | Bring from Penpot | |
|---|---|---|
| Makes | Each paired Penpot set match Stoaken: adds, updates and removes tokens | Each set you choose match Penpot: adds, updates and removes tokens |
| Leaves alone | Penpot’s own sets; tokens Stoaken can’t hold (text case, for instance) | Tokens Penpot can’t hold (motion, for instance) |
| Themes | Creates or updates the “Stoaken” themes | Not brought; themes are made in Stoaken. A new set joins the Brand’s first theme |
| Goes live | At once, in the Penpot file | Never 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 finds | What happens |
|---|---|
| The sets match the release | It shows what Penpot would take, and sends it. |
| The sets have changed since the release | It 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 release | It 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 live | It 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) | Penpot | Notes |
|---|---|---|
color | color | sRGB: #rrggbb, or rgba() with transparency. Other colour spaces stay in Stoaken. |
dimension | one of fontSizes, letterSpacing, borderRadius, borderWidth, spacing, sizing, dimension | Chosen from the token’s path (radius.md is a border radius). px are written as plain numbers; rem as 1.5rem. |
number | opacity, rotation or number | By path: opacity.disabled is an opacity. |
fontFamily | fontFamilies | A list of families. |
fontWeight | fontWeights | 400, or a named weight. |
typography | typography | Family, size, weight, line height and letter spacing. |
shadow | shadow | One 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:
"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 Stoaken | Stays in Penpot |
|---|---|
duration, cubicBezier, strokeStyle, border, transition, gradient: Penpot has no tokens for them | textCase, textDecoration, string, boolean, other: DTCG has no type for them |
| Colours outside sRGB; dimensions in units other than px and rem | Values 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:
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 dataTroubleshooting
| If | What to do |
|---|---|
| Penpot refuses the access token | It has expired or was deleted. Make a new one and reconnect. |
| The file isn’t found, or access is refused | Check the link, and that the token’s account can open the file. |
| The address can’t be used | Self-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 have | It’s created and joins the Brand’s first theme (a Default theme if there’s none), like a set added on Overview. |