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.

Project two

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.

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.