I’m a full-stack product designer who speaks engineering and thinks in systems.

SINA, a governed design system

I designed and built SINA solo in one month: a design system that decides what an AI may render.

2,926npm downloads across five packages, 10 July to 31 July 2026
5governance holes I found in my own system, all closed before 0.3.0
31accessible primitives under one rulebook, each with an automated accessibility test

Live at sinahub.app, the packages on npm and the source on GitHub.


Nobody in the chain asks if it is allowed

Software has started letting a model build part of the screen. The model decides what should appear, and the app renders it. Nothing in that chain asks whether the thing on screen should exist.

A model is trained to produce output that looks right. A component library draws whatever it is handed. Neither one knows what a wire transfer is, or that a large one needs a second person to approve it.

So an agent can put a confirm button for a $50,000 wire in front of a user. No approval step, no record of who clicked it. The output looks correct, which is exactly why it ships.

I had a second reason for this project. Well made components and a tidy guideline have become a commodity, and I wanted to find out what the next role of a design system is.

  1. SINA off

    1. The model proposes a wire dialogIt emits an intent and the props for a $60,000 transfer.
    2. The app renders itThe component library draws whatever it is handed.
    3. Reaches the user: Confirm button on screenNo approval step, and no record of who clicked it.
  2. SINA on

    1. The model proposes a wire dialogThe same intent, the same props.
    2. Gate: The server checks the rulebookZod checks the shape first, then the policy limits.
    3. Reaches the user: A governed dialog renders insteadIt demands a second approver, bound to these exact terms.
Same request, two pipelines. The gate runs on the server, so nothing unsafe ever streams.
Emulator showing an agent request being checked against the rules. Allowing the pass since the intent doesn't break any rules.
Emulator showing an agent request being checked against the rules. Blocking an intent and mounting Secure Wire Dialog.

A component that has streamed cannot be recalled

React streams UI to the client, and streamed output cannot reliably be revoked. A check that runs in the browser runs after the unsafe version is already on screen. That leaves one place where a check can matter: the server, before anything goes out.

Regulation is arriving anyway, and it will reach a solo builder as surely as a bank. A team that wants this protection today has to write the interception layer itself. Most might skip it, because governance is the first thing dropped when you are in a hurry.

I wanted a small team to be able to install it instead, with the rules already written and theirs to change. As a bonus they get a bundle of primitives and pre-built React components that already answer those rules.

Eleven phases of building SINA with AI playing a role of a developer.

I did not know React well when I started, and I knew governance mostly as a word and some high level use. Roughly half the month went on learning generative UI, governance and React properly, which left about two weeks to build in.

The first two days produced no code. I spent them on a roadmap that introduced each unknown in an order I could study first and implement second. The alternative was handing the whole thing to Claude and hoping, which is the failure this project exists to argue against.

It came out as eleven phases. Tokens and theme first, then the primitives, then the Zod rulebook. The playground came fourth, early enough that the AI integration met real model output rather than fixtures. Documentation, the publish pipeline, the marketing site and release hardening filled the second half.

That order is the reason this is a system and not a pile. Every phase had something I had to understand before Claude wrote a line of it.

n intent is a verb plus props: plain JSON the model emits, usually through a tool call:

const envelope = {
  intent: "wire_transfer",
  props: {
    amount: 6_000_000, // minor units: $60,000.00
    currency: "USD",
    debtor: { name: "Acme Corp", account: { scheme: "sepa", iban: "DE89…" } },
    creditor: { name: "Beta LLC", account: { scheme: "sepa", iban: "DE89…" } },
  },
};

The decisions that mattered

Three calls shaped the result more than the rest. Each one cost something I would rather have kept.

Killing Tailwind on day nine

  • The options: keep Tailwind and hand-write a token layer beside it, or take it out and rebuild on CSS variables.
  • The call: take it out, across the whole repo. Utility classes put the styling inside the markup of every component, so there is no single place to change what a colour means. In a system whose product is control, that made theming hard to govern and the validator hard to read.
  • The cost: two days and a lot of tokens, inside a one-month schedule. The documentation framework couples its own interface to Tailwind, so I rebuilt the docs chrome from SINA’s own primitives. That at least made the system its first customer.

Fintech before healthcare

  • The options: open with healthcare governance, or open with fintech.
  • The call: fintech. Its rules are published, so I could check my own thresholds against FinCEN and ISO without asking anyone for access. It is also the easier domain in which to argue for governance.
  • The cost: the second domain never arrived. Defence is descoped and nothing replaces it, so healthcare and defence appear on the landing page as canned simulations labelled “Coming soon”.

Validate, then mount. Never mount, then check.

  • The options: prompt the model carefully, check the props once they reach the browser, or gate every proposal on the server before anything renders.
  • The call: the server gate. The model emits intent as JSON, a Zod schema decides, and only then does a component mount. A refusal does not render an error. It renders the stricter component the rule requires.
  • The cost: nothing renders optimistically, and the routing is the host app’s job. If a request never reaches the gate, none of this holds, which is a boundary I now state in the documentation rather than imply.
import { evaluateFintechIntent } from "@sina-design-system/fintech";

const decision = evaluateFintechIntent(envelope);
{
  "valid": false,
  "violations": [
    { "code": "SAR_REVIEW", "severity": "flag" },
    { "code": "CTR_REPORTABLE", "severity": "flag" },
    { "code": "AMOUNT_REQUIRES_APPROVAL", "severity": "escalate" }
  ],
  "requiredComponent": "SecureWireDialog"
}
Severity never rides on colour alone, so a block is separated from an escalation by glyph and border as well.

I built a bank to attack my own system

Test fixtures only ask the questions I thought to write down. So I built a second application whose job was to break the first. PennyPinchr is a euro retail banking assistant with an invented backend, driven by a real model.

I installed SINA the way a random person would, from npm, working from the public documentation. Then I put a toggle on the front. Off is the app's own generative UI pipeline. On is SINA: intent, server gate, governed component.

The first thing it exposed was not a bug. An agent needs a machine-readable description of what each governed action accepts, and I had not shipped one. Version 0.2.0 added a JSON Schema for every verb, derived from the exact Zod object the gate runs. It also added a verb for when the agent has to ask a question rather than propose an action.

PAN request refused by SINA.
€60k wire intercepted
SecureWireDialog mounted.
Pass approved by SINA.

The €60,000 wire that passed clean

The worst finding came back valid, with zero violations, on a €60,000 wire. Every band was skipped for all seven non-USD currencies the schema accepted: dual control, Travel Rule, SAR, CTR.

The policy skipped every currency except US dollars. A comment in the code said why: matching those dollar thresholds in another currency was out of scope. Documented is not safe. A silent pass is the exact failure this system exists to prevent.

Four more came out of the same sweep. The step-up dialog told the user that an initiator cannot authorise their own action. In second-factor mode it checked neither that nor the binding to the terms, so I approved my own card cancellation and it passed.

A missing amount field rendered a plausible wire dialog reading $0.00 rather than failing. Fallback copy hardcoded $50,000 in a euro app, and the formatters were pinned to US English, so SINA showed “Jul 20, 2026” next to the host app’s “20 Jul 2026”.

I filed all five as bugs against my own project and fixed them between 0.1.0 and 0.3.0, checked again on 21 July 2026. In every mode, an approval is now bound to the exact terms on screen, and nobody can approve their own action.

Twenty-seven components took a language setting, so dates and amounts match the app around them. A dialog missing a required field now fails loudly instead of inventing a number. A transfer in another currency says plainly that the dollar thresholds were not applied, and a team can set its own thresholds.

The pattern matters more than the five bugs. The layers held and the defaults did not. Every failure was a policy default written from a US assumption, and each fix was small, because the extension surface was already exported.

An outsider could not tell what it was for

I asked two developers I have worked with before to look at it. The first response was the one I needed.

The page looks very nice. But I really don’t understand what problem it solves.

— A backend engineer, on the first landing page

They were right. The hero copy led with the mechanism, which only means something to a reader who already knows the problem. I had spent a month inside it. A visitor gets a few seconds.

The second reviewer, a frontend engineer, worked SINA out only by reconstructing the flow themselves. Their paraphrase became the fix: React already does conditional rendering all the time, and this is the same move with policy as the condition.

So I rewrote the four main sections of the page, three versions of each. One led with how it works, one with what goes wrong without it, and one said as little as possible.

Twelve drafts, and I picked a winner section by section rather than a whole set. The version that led with the problem won the top of the page, and gave me the headline: “AI agents will render anything. Yours shouldn’t.”

Every section since gets the same test. Does it name the problem before the mechanism?

A refusal is a component, not an error

What shipped is five packages under MIT. A token layer, 31 accessible primitives with no domain logic, the governance engine and its contract, the fintech rulebook as pure Zod, and the governed components that render its decisions. The primitives install on their own, with no AI and no gate involved.

Some design decisions are enforced rather than asserted. Severity never rides on colour alone, so a pass, an escalation and a block differ by glyph and border too. Both palettes are held to WCAG AA contrast by build guards, and every primitive carries an automated accessibility test.

The part I am most attached to is that the system is readable by the agents that use it. A machine-readable catalogue sits beside the human documentation: every component, token and rule, plus the payload schema for each of the fifty registered verbs. A model can consult the design system instead of guessing at it.

Did it work?

It shipped, publicly and completely, and it holds up under its own rules. Five packages sit on npm at 0.3.0, with 2,926 downloads between 10 June and 31 July 2026.

What held matters as much as what broke. An extra field slipped into an otherwise valid wire was rejected outright. The system refused to let me approve my own transfer, first try.

When someone approves a transfer, the server works out the terms again from its own records rather than trusting what the screen sent back. So approving €5,000 and then sending €60,000 cannot happen. The account-number check also caught two of my own test accounts as invalid.

The gaps are real, and I would rather name them than let someone find them.

This is early software, so the way other code talks to it can still change from one release to the next. It also keeps a record of every decision, but by default that record is thrown away. Nothing is stored until the team installing it points those records somewhere.

The next thing that would change this case study is a first outside integrator.


What I’d do differently

  • Build this with a developer from a fintech background. Public sources gave me the rules that are written down, and missed the ones practitioners carry in their heads.
  • Run the adversarial sweep in week one. PennyPinchr found five real holes in a single session, and I only built it once the packages were already public.
  • Decide the theming approach before the components exist. Nine days of Tailwind cost two days to undo, and the question that killed it, what theming would need, was answerable on day one.

Remarkable PRO, a production-ready suite

I led Remarkable PRO, a component suite and live docs hub for Embeddable that cut new users’ time to value by more than 80%.

80%+faster time to value for new Embeddable users, vs the vanilla-component setup, team estimate
60%+faster production of new components, vs the old Figma handover flow, team estimate
3 weeksof work lost redoing the token format, and every early adopter had to retheme

Live at remarkable.embeddable.com, the source on GitHub, the Figma library in the community.

Remarkable PRO Figma library cover.

Every component styled itself

Embeddable ships with a pack of vanilla components: basic charts, select menus for filters, the primitives that fill a first dashboard. They are handy for teams that arrive without components of their own and want to plug in their data and start. For a first-time visitor they did the job: a populated dashboard right away.

Every one of them carried its own hardcoded CSS, and no two agreed. Restyling meant editing components one at a time, and the results drifted. For a product whose whole promise is embedding into someone else’s app, that was a weakness waiting for a trigger.

Custom Canvas raised the stakes

The trigger was Custom Canvas, a feature that lets end users build their own dashboards with interface tools inside our customers’ apps. A dashboard builder living inside somebody else’s product has to look native there. Hardcoded styles cannot do that.

Theming stopped being a cosmetic option and became infrastructure. It had to be easy, and it had to keep scaling as new interface elements and dashboard components arrived.

Six months of persuasion, one month of building

The idea came from me, and nobody had asked for it. Design system literacy in the company was low, so the value was not obvious to the people who had to approve the work. Front-end preferred the processes it already had.

So for six months I connected real frictions in the product to what a system would fix, one concrete example at a time. Each example ended at the same place: faster time to value for new users. The persuasion took six times longer than the build, which ran from November to December 2025 with one PM and two engineers.

An open-source suite as the way in

The plan had four parts. Native integration, so a component feels like part of the host application, and a generation pipeline, so new components come out consistent instead of hand-made. Documentation rich enough that users can understand the token structure without asking us. And a Trojan horse: an open-source library attractive enough that people come for the components and stay for the platform.

The look plays on emotion on purpose. Different from sterile BI libraries, but not so opinionated that it fights the host app.

The sequence followed the same logic. Token grammar first, then a judgement per component on how granular to go, guided by user feedback, because some components genuinely need more unique tokens than others. We started with two basic charts and one controller, then extended by request: table, donut chart, scatter chart. A default and a dark theme shipped out of the box, so users had two themes to study and two to use.

Three token formats and a docs site failed first

Little of this worked on the first try. We went through three directions for the token format, and each grew confusing and refused to scale.

We also built a dedicated documentation page on Mintlify. Keeping it current was slow, manual and error-prone, and users still came straight to us with their theming questions. A static document could describe the system; it could not teach it. Both failures fed the decisions below.

The decisions that mattered

Redoing the token format under live users

  • The options: keep the format early Remarkable PRO users were already on, and accept the growing tangle and eventual stagnation, or stop and rebuild it properly.
  • The call and why: rebuild completely. The token grammar is the layer everything else stands on, and breaking it would only get more expensive as adoption grew.
  • The cost: three weeks of work thrown away, and every existing user had to switch components and retheme them.

Building our own live documentation

  • The options: stay on Mintlify, which the company already used everywhere, or build documentation shaped around the specific problems we kept hitting.
  • The call and why: build a bespoke live documentation hub. A granular system produces an absurd number of tokens, and order comes from a tool people can explore, including people with little technical background.
  • The cost: three months of staying on Mintlify while the hub came together, and the potential users lost in that window.

Designers build in code, not in Figma

  • The options: hand over designs with the new tokens in Figma, as established, or a pipeline where designers build live React components that engineers take upstream.
  • The call and why: break from the established practice. The Figma handover had stopped being efficient for this work; building the real component removes the translation step.
  • The cost: three months of the waterfall Figma-to-front-end approach before the new pipeline existed.

A grammar that makes every token predictable

The rebuild produced a naming convention with real structure: semantic, hierarchical and state-aware, so a token's name tells you what it does before you have ever seen it. The em- prefix keeps our tokens recognizable next to a customer’s own variables in their codebase.

The grammar stacks four layers, primitive to semantic to component to state. A name reads namespace, then scope, then property, with a state suffix when one applies.

<namespace>-<scope>-<property>[--<state>]

Primitive
    ↓
Semantic
    ↓
Component
    ↓
State

em-actionicon-background--hover
em-button-border--focus
em-input-text--disabled

Most of the project’s thinking time went into this grammar. It is the part that decides whether every token stays predictable, discoverable and easy to inspect, theme and scale.

Documentation hub AKA Sandbox.

The docs became a product

The documentation hub is a working app, not a manual. Users switch themes on live components, inspect an individual part of a component to see and extract the tokens behind it, and try props directly on the page. Someone with little technical background can explore it and learn how to theme.

Underneath, it is two design systems running in one environment. I used Claude to combine them: Embeddable’s design system powers the app shell, and Remarkable UI powers the experimental component layer. That combination turned the documentation into a sandbox app, a place to explore, prototype, test and demonstrate components inside a real product environment.

  1. Embeddable design system

    1. Powers the app shellNavigation, layout and the page chrome around the demos.
    2. Stable, already in production
    3. Reaches the user: One sandbox appWhere users read the docs and we prototype.
  2. Remarkable UI

    1. Powers the component layerCharts, controls and the themes on display.
    2. Experimental, changes fast
    3. Reaches the user: One sandbox appReal components, in a real product environment.
Two design systems, one app. The shell is stable and the component layer is not, which is what makes the hub a sandbox as well as documentation.
Inspect shows the tokens behind each part of the component, there to see and extract.
Component view in the sandbox.

Prototypes flow upstream

The sandbox also changed how components get made. I prototype new components directly in it, behind an Auth0 login that reveals the work in progress. AI generates the tokens and breaks the component into CSS modules under the naming convention. After testing, demos and iteration, front-end picks up the code and implements it properly upstream.

The handover artifact is a working component, no longer a static Figma file. A Figma library still covers the basics for designers who want it. Later we wrote a skill that lets AI build future components the same way, and another that helps our users theme them properly.

  1. In the sandbox

    1. I build the prototypeA live React component, not a picture of one.
    2. AI generates the tokensThe component is split into CSS modules under the naming convention.
    3. Gate: Tested, demoed, iteratedIt only leaves the sandbox once the team agrees it is right.

    Behind the Auth0 login, so work in progress stays private.

  2. Upstream

    1. Front-end picks up the code
    2. Code review and integrationThe developers own this half, and their reviews come back to me.
    3. Reaches the user: Shipped in Remarkable PROA mature component, in the suite and in the docs hub.

    The production codebase the customers actually get.

The pipeline that replaced the Figma handover. Design owns the left column, front-end owns the right, and the artifact that crosses between them is code.
A sandbox prototype in code.

Did it work?

Remarkable PRO is one of the most popular parts of Embeddable, and the part the company credits for conversion. Harry Marshall, Embeddable’s COO, put it this way:

Single most important factor in getting users to value quickly and them subsequently choosing to use Embeddable.

— Harry Marshall, COO, Embeddable

Two numbers travel with the project, and both are team estimates rather than instrumented metrics. Time to value for a new Embeddable user fell by more than 80% compared with the vanilla-component setup. Producing a new Remarkable component became more than 60% faster than under the old Figma handover flow. Neither number separates the suite cleanly from everything else Embeddable shipped in the same period, which is why I call them estimates.

Agreeing who owns what

Convincing the decision-makers took patience and positive reassurance far more than clever argument. The harder negotiation came afterwards, when I joined the front-end pipeline directly and we had to agree who owns what.

We settled on a clean split: I own the tokens and the CSS, and the developers own code review and integration. Once ownership was settled, the friction turned into feedback. Their reviews sharpened my decisions on every component that followed.

What I’d do differently

I would lead the persuasion with documentation. It looks like a supporting artifact, yet it is the part that makes a granular system possible to understand and to scale. Explaining that earlier might have shortened the six months.

And I wish we had started six months sooner. I think it would have changed our overall strategy, and I offer that as an assumption rather than a fact.

Project three

A one-sentence framing of the project: who it was for, what it had to achieve, and the shape of the outcome — enough that a skimming reader knows whether to stay.

The finished product in context. Captions carry the one detail the image can’t say for itself.

The problem

Two or three paragraphs of situation: the state of the product before the work, the user pain it produced, and why now. Ground it in something observed — a support-ticket theme, a funnel number, a research session — rather than opinion.

A second paragraph for the constraint that shaped everything: the deadline, the legacy system, the platform rule. Naming the constraint early is what makes the later decisions read as decisions.

Goals

  • The primary user outcome, phrased as a change in behaviour.
  • The business result that outcome was expected to move.
  • The quality bar — accessibility, performance, brand.

Process

  1. Research: what was looked at and who was spoken to.
  2. Exploration: the directions tried, and what killed the losers.
  3. Validation: how the surviving direction was tested.

A short verbatim quote from research or launch feedback — the sentence that reframed the problem or proved the solution.

— Participant 4, usability study

The solution

What shipped, described as the user meets it. Lead with the flow, not the screens; the images below carry the pixels.

A detail worth zooming into

One subsection per design decision that deserves its own heading — the interaction model, the empty state, the edge case that shaped the happy path.

TaskBeforeAfter
Complete first setup6m 40s1m 55s
Find an existing record58s12s
Task success rate64%93%

Where the work touched implementation, show it — a token, a config, an API shape. Code earns its place when the design decision lives in it:

:root[data-theme="dark"] {
  --panel: #33302d; /* one card step off the page base */
}
Before: the original flow.
After: the shipped design.

Outcome

−71%time to first success
+29pttask success rate
4.8post-launch CSAT

A closing paragraph: what the numbers meant, what was learned, and the one thing that would be done differently next time.