Documentation Index
Fetch the complete documentation index at: https://docs.moda.app/llms.txt
Use this file to discover all available pages before exploring further.
Design-to-code in CI
Problem: You have a canonical Moda canvas that represents your design tokens (colors, fonts, radii, spacing variables). On every push to main, regenerate tailwind.theme.ts (or equivalent) from the canvas and commit the diff, so code always tracks design.
Primitives
GET /v1/canvases/{id}/tokens — structured JSON of colors / fonts / radii / variables
- A tiny codegen step in your CI (Node / Python / Deno)
- Commit + PR if the diff is non-empty
Scope: designs:read only. No tasks:write, no designs:export. Create a minimal API key for CI.
TypeScript — GitHub Actions
.github/workflows/sync-theme.yml:
name: Sync design tokens
on:
push:
branches: [main]
schedule:
- cron: "0 9 * * 1" # also weekly on Monday 09:00 UTC
permissions:
contents: write
pull-requests: write
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: "20" }
- name: Regenerate theme
env:
MODA_API_KEY: ${{ secrets.MODA_API_KEY_READONLY }}
CANVAS_ID: cvs_01HT9WK8N3M2J4A5Z6P7Q8R9TV
run: node scripts/sync-theme.mjs
- name: Open PR if changed
uses: peter-evans/create-pull-request@v6
with:
branch: design-tokens-sync
title: "chore: sync design tokens from Moda"
commit-message: "chore: sync design tokens from Moda"
body: "Auto-generated from [canvas](https://moda.app/canvas/${{ env.CANVAS_ID }})."
scripts/sync-theme.mjs:
import fs from "node:fs";
const API = "https://api.moda.app/v1";
const HEADERS = {
Authorization: `Bearer ${process.env.MODA_API_KEY}`,
"Moda-Version": "2026-05-01",
};
const res = await fetch(`${API}/canvases/${process.env.CANVAS_ID}/tokens`, {
headers: HEADERS,
});
if (!res.ok) {
const err = await res.json().catch(() => null);
console.error("Moda fetch failed:", err?.error ?? res.statusText);
process.exit(1);
}
const { variables, colors, fonts, radii } = await res.json();
// Emit a predictable, diffable TS file.
const body = `// AUTO-GENERATED — edit the Moda canvas instead.
// Source: cvs_${process.env.CANVAS_ID}
// Generated: ${new Date().toISOString()}
export const theme = {
colors: {
${Object.entries(variables)
.filter(([, v]) => typeof v === "string" && v.startsWith("#"))
.map(([k, v]) => ` ${JSON.stringify(k)}: ${JSON.stringify(v)},`)
.join("\n")}
},
palette: ${JSON.stringify(colors, null, 2).replace(/\n/g, "\n ")},
fonts: ${JSON.stringify(fonts, null, 2).replace(/\n/g, "\n ")},
radii: ${JSON.stringify(radii, null, 2).replace(/\n/g, "\n ")},
} as const;
`;
fs.writeFileSync("src/design/theme.ts", body);
console.log("Wrote src/design/theme.ts");
Python — GitLab CI variant
.gitlab-ci.yml:
sync-theme:
image: python:3.12
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
- if: $CI_COMMIT_REF_NAME == "main"
before_script:
- pip install httpx
script:
- python scripts/sync_theme.py
- |
if ! git diff --quiet src/design/theme.py; then
git config user.email "ci@example.com"
git config user.name "CI"
git checkout -b design-tokens-sync
git add src/design/theme.py
git commit -m "chore: sync design tokens from Moda"
git push origin design-tokens-sync -o merge_request.create
fi
variables:
MODA_API_KEY: $MODA_API_KEY_READONLY
CANVAS_ID: cvs_01HT9WK8N3M2J4A5Z6P7Q8R9TV
scripts/sync_theme.py:
import os, sys, datetime, httpx
HEADERS = {
"Authorization": f"Bearer {os.environ['MODA_API_KEY']}",
"Moda-Version": "2026-05-01",
}
r = httpx.get(
f"https://api.moda.app/v1/canvases/{os.environ['CANVAS_ID']}/tokens",
headers=HEADERS,
timeout=30,
)
if r.status_code != 200:
print("Moda fetch failed:", r.json().get("error"))
sys.exit(1)
tokens = r.json()
generated_at = datetime.datetime.utcnow().isoformat(timespec="seconds") + "Z"
with open("src/design/theme.py", "w") as f:
f.write(f'''"""AUTO-GENERATED — edit the Moda canvas instead.
Source: {os.environ["CANVAS_ID"]}
Generated: {generated_at}
"""
COLORS = {tokens["variables"]!r}
PALETTE = {tokens["colors"]!r}
FONTS = {tokens["fonts"]!r}
RADII = {tokens["radii"]!r}
''')
print("Wrote src/design/theme.py")
Why tokens, not get_canvas
GET /v1/canvases/{id}/tokens is a dedicated endpoint that returns structured JSON. Parsing tokens out of the pseudo-HTML from GET /v1/canvases/{id} works but is fragile — layer names / HTML shape can change without signaling a token change. Use the dedicated endpoint for CI.
Making it diffable
- Sort all arrays before emitting — otherwise insertion order in the canvas creates spurious diffs on every run.
- Emit a deterministic timestamp header (or omit the timestamp entirely; git tells you when the file changed).
- Name variables in the Moda canvas — named variables land as keys in the
variables object and become your colors.primary, colors.background, etc.
Gotchas
- Scope the API key narrowly.
designs:read only. A leaked CI key with tasks:write / designs:export is a bigger blast radius.
- Unknown response fields may appear over time. Don’t fail the build if the JSON has new keys — only fail if the keys you need are missing.
- Cache-bust properly. If your codegen reads other files (a base theme, palette overrides), include them in the cache key for the CI action.
- Don’t bypass PR review by committing directly to
main. PR the change — design tokens can have visual fallout.
- The canvas must be team-accessible to the API key’s team. Share links do NOT grant design token reads in CI — use a canvas URL + a team-scoped key.
See also