Introducing the Feeef Template Design System
Why themes are documents, how JSON and React meet, and what that means for merchants, developers, and the storefront.

I'm Mohamed, the developer behind the Feeef template system.
I want to explain this system in the way I think about it when I am working on it: from the merchant's smallest edit all the way down to the code that renders a page. The JSON, the React components, the visual editor, the server, the CLI, and the design system belong to the same story.
A storefront can be beautiful in a screenshot and still be a difficult product to live with. The real test comes later. Can the merchant change the heading? Move the phone field? Change the brand color without breaking contrast? Install an update without being surprised? Can another developer understand what the first developer intended?
Those questions matter to me because the person running a store should have real control over it. Every time a simple content change requires a developer, some of that control disappears.
At the same time, I do not want template authors trapped inside a tiny collection of predefined blocks. I want them to write real components, build distinct visual identities, and use the tools they already understand.
The Feeef Template Design System is my attempt to make those two needs meet. The central decision is simple to say and much harder to implement:
A theme is a structured document that a shared storefront knows how to render. Developers author its capabilities; merchants edit its instances.
This post describes the architecture in the current codebase. Some implementation details go further than the public overview, particularly around isolated SSR. I will distinguish the design goal from the guarantees the implementation actually provides.
What I mean by a template design system
There are two kinds of design here.
One is visual: typography, colors, spacing, borders, motion, mobile behavior, and the states of a button or a field. A theme needs a coherent answer to those questions.
The other is structural: what a component is, what can be edited, where another component can be inserted, how identity survives a save, and which parts of an order flow belong to the platform.
A palette alone cannot answer those structural questions. A component renderer alone cannot make a theme visually coherent. Feeef connects both through a document model, an editor schema, a React runtime, and a written visual contract inside each theme package.
Lithium is the hosted Next.js storefront. The merchant editor is written in Flutter. Template authors work in TypeScript and TSX. These environments do not share a React component tree in memory. They share a serializable contract called TemplateData.
That cross-language boundary is one reason JSON matters so much. Flutter can inspect it, the backend can store it, the CLI can produce it, and Lithium can interpret it. The editor does not need to execute React to know that a component has an editable heading and a slot called content.
The first decision: ship a document
A conventional custom storefront gives the developer an application to deploy. That is a good model when the developer owns the application for its whole lifetime.
A marketplace template has a different relationship with its owner. The author builds it, then other people install it and change it. Feeef already runs the storefront infrastructure. Requiring another storefront deployment for every theme would turn template distribution into application operations.
Instead, the author's package compiles into artifacts that the existing storefront consumes:
TSX source + metadata + schema + locales
↓
feeef template build
↓
dist/data.json + dist/schema.json
+ dist/locales/
↓
preview, publish, or install
↓
hosted Lithium storefrontA template author still has a normal project folder, npm dependencies, types, and Git history. The published unit is the compiled template, rather than a separate application server.
The cost is that Feeef owns a runtime contract. A theme cannot assume it has an arbitrary Next.js application, its own server routes, or unrestricted server dependencies. It has the capabilities Lithium exposes. That restriction is part of what makes a theme portable between Feeef stores.
And “portable” needs a precise meaning. The JSON can move between tools and stores that understand the contract. A custom React component is still tied to the Feeef scope API. Serializing it does not make it executable on every commerce platform.
The document and the schema have different jobs
The system separates the actual storefront from the description of what the editor can offer.
| Part | Job |
|---|---|
TemplateData | Stores the actual pages, component placements, values, and composition |
TemplateSchema | Describes the catalog, page definitions, and editable controls |
| Flutter editor | Changes the document through those controls |
| Lithium renderer | Turns the document into the shopper's interface |
| Template kit and CLI | Compile developer source and distribute the artifacts |
A simplified, valid document looks like this:
{
"props": {
"theme": { "mode": "system" }
},
"pages": {
"home": {
"props": {},
"sections": {
"main": {
"components": [
{
"type": "custom",
"instanceId": "home_hero",
"title": "Home hero",
"props": { "heading": "Made for your everyday" },
"propsSchema": {
"heading": { "type": "string", "name": "Heading" }
},
"code": "function App() { return <h1>{props.heading}</h1>; }"
}
]
}
}
}
}
}The schema tells the editor that heading is a string with the label “Heading.” The value in props.heading belongs to this particular placement. Another placement can use the same component with different content.
The top-level schema also describes pages and the available component catalog. A theme's schema.ts overlays Lithium's built-in schema and produces dist/schema.json.
One detail deserves attention: an overlay containing pages replaces the page map. It does not automatically preserve every omitted page. A theme author must include the page definitions the theme needs. A route existing in the storefront does not mean the document contains useful content for it.
Separating schema and data creates two artifacts to understand. I accept that cost because the alternatives become confusing quickly: changing the component catalog and changing one merchant's heading are different operations.
A component node is a small agreement about ownership
The component node carries several kinds of information:
| Field | Meaning |
|---|---|
type | Selects a built-in renderer, a layout, custom code, or a reference |
instanceId | Identifies this placement across edits |
title | Labels the block in the editor |
props | Holds the instance's configurable values |
code | Holds custom component implementation |
propsSchema | Describes editable values |
children | Holds nested nodes for layout components |
slotsSchema | Declares named insertion points |
slots | Holds the merchant's composition inside those points |
slotsLayout | Describes a responsive slot arrangement that editor and runtime helpers can interpret |
refId, refVersion | Carry library-reference information; version pinning has limitations discussed below |
code, propsSchema, and the slot fields sit beside props. They are not nested inside it. That boundary lets the editor distinguish an implementation from an editable value.
It also expresses an intention: the developer defines capabilities, while the merchant controls the content and composition those capabilities expose. This is a modeling boundary, not an access-control mechanism. Authorization and validation still belong on the backend.
There is a useful test for any custom component: if the merchant must edit its function to change ordinary content, its editable interface is incomplete.
Expose the heading, image, and call to action through props. Expose meaningful composition through slots. Avoid exposing every internal CSS detail as a control; an inspector with eighty unrelated settings can make a merchant less confident, not more capable.
A good schema is part of the component's product design.
One component, your content
Edit the props. The implementation and placement identity stay the same.
Made for your everyday
Stable identity: home_hero
Instance data
{
"type": "custom",
"instanceId": "home_hero",
"props": {
"heading": "Made for your everyday",
"hide": false
}
}Inspect the complete example node
{
"type": "custom",
"instanceId": "home_hero",
"props": {
"heading": "Made for your everyday",
"hide": false
},
"propsSchema": {
"heading": {
"type": "string",
"name": "Heading"
},
"hide": {
"type": "boolean",
"name": "Hide section"
}
},
"code": "function App() { if (props.hide) return null; return <h1>{props.heading}</h1>; }"
}An illustrative model of the contract. No custom code is evaluated here.
Stable identity is part of the experience
instanceId identifies a placement, not a source file or a position in an array.
The editor uses identity to keep track of selection. React uses it in reconciliation. Regenerating identifiers on every rebuild or save can remount components, reset local state, and move the editor's focus away from what the merchant was changing.
Two placements of the same header need different instance identifiers. Updating the code of an existing placement should preserve its identifier.
There are positional fallbacks in the renderer, but a fallback cannot offer the same continuity when a list is reordered. Stable identity is worth treating as a contract from the beginning.
Why allow React code inside JSON?
This is the decision that deserves the most scrutiny.
A purely declarative system with only built-in component types is easier to constrain. Every renderer is platform-owned. But the catalog also becomes the ceiling of what an author can build unless the platform keeps adding new types.
Feeef supports built-ins and custom components. A custom node stores a component implementation as a string. Developers usually write that implementation in a real TSX file; the compiler produces the string.
For example:
const propsSchema = {
heading: { type: "string", name: "Heading" },
hide: { type: "boolean", name: "Hide section", default: false },
} as const;
export const meta = {
type: "custom",
title: "Editorial heading",
order: 0,
propsSchema,
props: { heading: "", hide: false },
} as const satisfies FeeefComponentMeta<typeof propsSchema>;
type Props = FeeefLivePropsOf<typeof propsSchema>;
function App() {
const p = props as Props;
if (p.hide) return null;
return (
<section
style={{
color: "hsl(var(--foreground))",
background: "hsl(var(--background))",
paddingBlock: "3rem",
paddingInline: "1.5rem",
}}
>
<h1>{p.heading || t("home.heroHeading")}</h1>
</section>
);
}meta keeps the implementation and its editable interface close together. order sorts siblings during compilation. The entry point is function App(). The generated theme types describe the injected globals and derive prop types from the schema.
Source files can import local helpers and supported npm packages. The build bundles dependencies into the component's code body and removes module syntax from the published runtime code. React and React DOM stay external so the host supplies its React runtime.
This is why installing React inside a theme and treating it as an independently mounted app is the wrong mental model. A theme component runs inside the storefront's existing environment.
There are real costs to this design:
- Bundling dependencies per component can duplicate helper code across component bodies.
- Bundled JavaScript is harder to inspect than the original source modules.
- Dependencies must fit the browser environment and the live-code policy.
- Executable strings need evaluation, security controls, and useful error reporting.
- TypeScript protects authoring, but its types disappear at runtime.
Putting code in JSON does not make it safe, small, or purely declarative. It makes the distribution unit self-contained. The platform still has to account for everything that code can do.
The scope API replaces runtime imports
At execution time, Lithium supplies a scope containing the capabilities a component can use.
That includes React, instance props, translation helpers, navigation, store and product access, cart access, and slot-context helpers. Examples include useStore(), useCurrentProduct(), useFeeefCart(), useFeeef(), Link, useRouter(), t(), SlotContextBridge, and useSlotContext().
I prefer explicit calls such as React.useState() because they make the dependency visible and match the authoring contract. Hooks still have to obey React's rules. A global available in the scope is not permission to call a hook conditionally.
Data hooks also need null checks. Components can appear on different surfaces, and the isolated server snapshot does not reproduce every browser provider.
The scope API is a compatibility surface. Adding a helper is easy; changing its meaning can break installed themes. This is one of the long-term responsibilities of choosing a shared runtime.
For internal links, authors use the injected routing components. For order submission, they use the storefront's supported order flow. A component should not invent another commerce backend merely because it can make a browser request.

Children and slots solve different composition problems
A grid, flex, or container holds ordinary children. That is generic nesting: put these nodes inside this layout.
A custom component exposes named slots. That is a more specific promise: this component has a place for the gallery, a place for form content, or a place for supplementary details.
A product shell might declare:
{
"slotsSchema": {
"gallery": { "name": "Product media", "maxChildren": 1 },
"content": { "name": "Product details and order form" }
},
"slots": {
"gallery": [],
"content": []
}
}Lithium renders the slot nodes and passes the results into the custom component as props.slots. The shell chooses where to paint them:
function App() {
return (
<div className="product-shell">
<aside>{props.slots?.gallery}</aside>
<main>{props.slots?.content}</main>
</div>
);
}Declaring a slot does not render it. The implementation must paint it, directly or through SlotsLayout. slotsLayout is not a separate automatic renderer; the helper lets a component honor the configured responsive arrangement. A shell that ignores that arrangement cannot promise that editor layout changes will appear on the storefront.
Named slots give merchants freedom within an interface the developer can reason about. They also introduce more structure to validate, more identities to preserve, and limits on nesting. The renderer and resolver enforce depth guards so a malformed composition cannot recurse indefinitely.
I do not want nesting for its own sake. Use a folder when a component needs slots, children, or colocated helpers. Keep ordinary leaf components as flat files. The tree should describe a meaningful relationship in the interface.
The order form is where the model becomes practical
A product page is more than a layout. It coordinates product selection, offers, quantity, customer details, delivery, totals, and order creation.
If all of those live in one closed component, the merchant's smallest change becomes a source-code change. If each field implements its own order submission, shared behavior drifts.
The kit's order-form shell and independent fields give those responsibilities different homes. The shell owns shared state and submission behavior. Fields read that state through a context bridge, such as useSlotContext("pdp"), and the merchant can arrange them as nodes in a slot.
Name, phone, address, state, city, quantity, offers, and the buy button can have their own editable interfaces. Related fields can still share a visual row.
A hidden field stays in the document and renders null. Deleting the node is a separate operation. That distinction preserves its settings and position when a merchant turns it back on. It only works when the component actually honors hide; adding a Boolean prop without using it changes nothing.
Orders go through submitFeeefOrder, which posts to POST /api/orders/create, the storefront server route. That is the supported path today. The supported shell coordinates draft behavior and protections such as the honeypot and submission timing, so theme fields reuse that flow instead of posting orders themselves. A direct browser POST to the feeef API is coming, but it is not recommended until backend enforcement for it ships. Those protections are layers of the flow, not a promise that unwanted submissions become impossible.
There is a broader rule here: rearranging a form should not require rewriting its transaction logic.
Reuse has two different meanings
Feeef has both source-level reuse and runtime library references. They can look similar in a project, but their behavior differs.
Build-time references
Inside the kit, a placement can reference a shared component:
{
"$ref": "shared.header",
"instanceId": "header_home",
"props": { "sticky": true }
}The build resolves $ref and inlines the component into dist/data.json. No kit $ref remains in that document.
This makes the build artifact self-contained. Updating shared.header updates its placements in the next build. It does not independently mutate every previously installed theme.
The price is duplication in the generated artifact. Source reuse and network deduplication are different things.
Runtime references
A runtime placement has another shape:
{
"type": "reference",
"refId": "cmp_example",
"props": { "sticky": true },
"slots": {}
}The server collects reference identifiers, batch-resolves accessible component records, and materializes custom nodes before rendering. Publishing can also move compiled component bodies into that library and rewrite placements as references, including components originally authored inline on a page.
Library defaults merge with instance values. Instance props win; nested objects can merge, while arrays are replaced. Slot overrides work by slot identifier. An explicit empty slot array means the merchant emptied that slot, so it must win over the library default.
Unresolvable references are skipped. This keeps one deleted or inaccessible library entry from crashing the page, but an omitted critical block can still make the storefront unusable. “Fail-soft” describes rendering behavior, not business success. Missing-reference diagnostics matter.
There is also an important versioning limitation: the current storefront resolver carries a refVersion field but does not enforce it as a version-selection target. Do not assume adding that field gives a placement immutable dependency pinning. Installation has a separate backend materialization path, and whole-template releases have their own snapshots.
A robust update story must consider the template document, referenced code, translations, and the host runtime together. The backend snapshots translations with releases and materializes component references when installing a buyer's working copy. That protects the installed copy from remaining dependent on private author-library records. However, a release document that still contains references is not itself an immutable snapshot of every referenced code body: materialization can consult the library state at installation time. A version number on one layer cannot freeze the others by itself.

What actually happens on a storefront request
The conceptual flow is:
Follow a storefront request
Explore the boundaries between the document, server, worker, and browser.
Select document
Load the store and select its live document, draft preview, or release preview. Materialize accessible component references before rendering.
TemplateData + resolved component bodiesWorker rendering is optional and uses a limited snapshot. Full SSR parity is not guaranteed.
Read the complete pipeline as text
Request for a store and route
↓
Load store + select effective template
↓
Draft preview, release preview, or live document
↓
Materialize accessible component references
↓
Prepare active page + serializable data snapshot
↓
Validate and compile custom code
↓
Optional isolated static rendering
↓
Send route-specific template payload
↓
Built-ins + custom markup + browser interactivityThe registry maps a node's type to a renderer. Layout types recurse through children. Custom types receive their compiled implementation and rendered slots. Unknown or unavailable types can render nothing.
Standard page identifiers connect the document to the hosted routes: home, products, product, checkout, and thank_you, with additional surfaces such as contact and landing pages. The author supplies the page composition inside those supported surfaces.
For pages such as product, checkout, and thank-you, the single-main model puts detailed layout inside the page's composition, commonly through a custom shell and slots. That gives the editor a clear root without requiring the platform to invent a named section for every possible layout region.
SSR is three separate questions
The phrase “server-side rendering” can hide several different operations. I want to be precise about what Feeef does.
1. Does the server compile the code?
Yes. Lithium prepares compiledCode on the server. Shoppers do not need to download a TSX transpiler to compile their theme in the browser.
That removes a specific source of browser work. It does not remove custom JavaScript from the browser, because interactive components still need an executable implementation.
The original code and the route's compiledCode have different roles. The former is an authoring artifact; the latter is prepared for evaluation by the live runtime.
2. Does the server execute custom code to produce HTML?
In the child-process runner path, yes. The runtime validates the source, transforms it, evaluates it in a separate Node process, and calls renderToStaticMarkup. The result includes ssrHtml alongside compiledCode.
The main Lithium process does not evaluate merchant component bodies in that SSR path. CustomLive paints the supplied HTML on the server.
The isolated worker receives a limited, serializable snapshot: props and selected slot-context data. Its scope is deliberately reduced. Several commerce hooks return null, translation returns keys, locale helpers use fixed defaults, and SlotsLayout is a stub. The snapshot is not the browser's complete provider tree, and slot React elements are not simply transported across the process boundary.
This matters in practice. A component whose initial output uses ordinary props may produce useful static markup. A component that requires useStore(), translation resolution, or a slotted shell can produce incomplete or empty output in the worker. Data-aware components need an appropriate seed and an initial render that tolerates this environment.
The current implementation does not guarantee full custom-component SSR parity with the browser. That includes content, localization, composition, and behavior.
3. Does the browser attach behavior to exactly the same tree?
The browser evaluates compiledCode against the real client scope and renders the resulting component. The server branch of CustomLive paints an HTML string; the browser branch renders React elements.
The wrapper uses suppressHydrationWarning. That suppresses some diagnostics; it does not prove the two outputs match. I would not describe this as ordinary, seamless React hydration of an identical server-rendered tree.
Authors still need deterministic initial output. Reading browser-only state during the first render, using random values, or replacing server-seeded content immediately can produce a visible change or hydration recovery. An SSR string is useful only if the shopper's transition to the interactive component is stable.
The deployment environment changes the result
The runner needs an environment that can spawn child processes. The current code detects Vercel, AWS Lambda, edge runtime, or an explicit pool-disable setting and takes a compile-only branch there. That branch returns compiled JavaScript with empty static markup.
A timeout or other non-policy runner failure can also fall back to compilation without HTML. A policy rejection blocks the component output instead.
Those branches preserve a path to browser rendering, but they do not give the same first-paint or no-JavaScript behavior. Built-in components can still have their normal host rendering behavior; a custom component without ssrHtml initially has an empty wrapper.
SSR claims therefore need to name the component, data requirements, and hosting path. The existence of an SSR runner is not evidence that every live deployment uses it or that every custom theme is fully visible before JavaScript.
Security belongs inside the architecture
Storing a React function as a string means accepting executable input. The sensitive boundary is especially clear on the server: the main application has access to credentials and internal services that a theme must not inherit.
The current design puts several controls in that path.
The live-code policy parses the source into an AST and checks for forbidden identifiers and calls, dynamic imports, dangerous JSX properties, JavaScript URLs, obvious infinite loops, and source and complexity budgets. Current defaults include a 120,000-character source limit and a 25,000-node AST budget.
The worker pool launches child processes with a reduced environment. It has a configurable V8 old-space limit, execution timeout, and recycling interval. Current defaults are two workers, an 800 ms job timeout, a 64 MB old-space setting, and recycling after fifty jobs. Those are implementation defaults, not a service-level promise or a total process-memory cap.
Results are cached, failures are measured, and a per-store circuit breaker limits repeated failing runner work. Marketplace validation adds another opportunity to reject problematic code before distribution.
Each layer solves a different problem. Static analysis catches known patterns. The process boundary separates evaluation from the main application process. Timeouts let the parent kill an execution that does not return. Recycling reduces long-lived worker accumulation.
None of those facts means a child process is a complete security sandbox. The worker still uses new Function, shares the host's operating-system permissions unless deployment adds restrictions, and is not automatically denied filesystem or network access by the fact that it is a child. A reduced scope and an AST denylist cannot prove that arbitrary JavaScript is harmless.
Similarly, client-side code executes in the storefront's browser environment. A server process boundary does not isolate the shopper's DOM or eliminate browser-side abuse. Stronger OS isolation, constrained egress, browser policies, dependency review, and adversarial testing remain meaningful concerns.
I want the security explanation to be as concrete as the rendering explanation. “Validated” and “isolated from the main process” describe actual layers. “Safe because it is JSON” would describe nothing useful.
The page should carry only what it needs
The document is useful to the editor in its full form. The shopper does not need the full editing contract or the executable bodies of every page.
Lithium's slimming pass clones the effective document, replaces the client schema with a minimal stub, strips editor metadata such as prop and slot schemas, and prepares custom code for the active page. Inactive page bodies are stripped while some structure remains.
This reduces the client payload without mutating the canonical editable document. It is route-specific preparation, not a claim that the browser receives only a single tiny component.
There is a navigation consequence. A root layout may survive a soft navigation. A template slimmed for the home page cannot be reused blindly for the product page, because the product code was removed from it. TemplateRouteProvider supplies freshly prepared template state for the destination route.
A theme that works after a hard refresh but goes blank through an internal link may have a route-provider problem. The fix belongs in the rendering pipeline rather than in the theme author's markup.
Caching has a similar correctness requirement. The runner's memory cache hashes code, props, and slot context. The Next data-cache layer adds store, template version, instance identity, and snapshot information, with store-specific tags and revalidation.
Caching by code alone would be wrong: identical code with a different heading or product seed can produce different HTML. Any new snapshot input that affects output needs to participate in cache identity. Private shopper-specific data would require a separate analysis before entering a shared cache.
The performance tradeoff is real. Warm workers and cached output save repeated work; cold workers, queueing, snapshot serialization, and invalidation add operational complexity. A job timeout also does not by itself establish a bounded end-to-end request time, because queueing happens before execution.
The visual design system is a written contract
The runtime handles structure. A theme's design-system.md explains what that structure should feel like.
It records the audience, typography, spacing, radii, borders, component states, motion, responsive behavior, order-form styling, dark mode, currency display, and direction rules. It also explains decisions that a token table cannot express: whether cards are dense or spacious, how errors appear, which elements stay sticky, and where motion would get in the shopper's way.
This is particularly valuable when several people or coding assistants contribute to a theme. Without an explicit contract, each component can look reasonable by itself while the whole storefront loses coherence.
The file is guidance, not an executable validator. A filled-in document can still disagree with the shipped interface. Review and real previews close that gap. When the visual decisions change, the written contract should change with them.
Native tokens connect the theme to the merchant's brand
Store decoration values originate in a Flutter color representation. Lithium converts them into CSS variables. Custom components consume tokens such as:
.theme-card {
background: hsl(var(--card));
color: hsl(var(--card-foreground));
border: 1px solid hsl(var(--border));
border-radius: var(--corners-card);
}
.theme-button {
background: hsl(var(--primary));
color: hsl(var(--primary-foreground));
border-radius: var(--corners-buttons-large);
}Dark, light, and system modes use the host's token system. A merchant's radius settings can affect both built-ins and custom components that honor the tokens.
Derived dark colors are a useful default, but they do not guarantee every pairing in every custom composition has sufficient contrast. Authors still need to check actual text, surfaces, focus rings, and disabled states in both modes.
Runtime code strings also complicate utility CSS generation: the application's build scanner cannot be assumed to discover every class an author later publishes. Native tokens and explicit styles are dependable tools at that boundary. Do not assume an arbitrary new Tailwind utility will exist merely because its name appears inside a code string.
Localization and RTL belong to the foundation
A Feeef theme needs to work in the languages its merchants use. Arabic, French, and English expose different assumptions about width, alignment, text, and numbers.
Theme translations live in locales/*.json and are read through t(). The local build embeds the bundle in data.i18n and writes locale artifacts. Publishing stores translations in store_template_locales; the storefront loads that bundle, with embedded translations available as a fallback for drafts and older documents.
The current active language comes from the store's defaultLanguage, normalized to a supported locale. It is not a complete browser-negotiated language switcher. Per-locale prop overrides and a visitor-facing language-selection flow should not be implied by the existence of locale files.
Interpolation supports simple named parameters. It is not a full ICU pluralization engine. A count-dependent sentence needs deliberate handling rather than an assumption that grammar will be supplied automatically.
Merchant copy and translated defaults also have different roles. A literal prop can override the theme's default translated heading. That is useful ownership, but it means a merchant-entered sentence does not magically translate itself.
For layout, use logical properties such as padding-inline and margin-inline-start. Phone digits may need an LTR input inside an RTL form. Direction is a component behavior to test, not a flag that repairs every left/right assumption.
Currency has a similar split: display the configured symbol to the shopper, and send the currency code in order payloads. Product descriptions go through the built-in product_body renderer for GitHub Flavored Markdown. Reimplementing that parser in each theme creates inconsistent rendering and unnecessary HTML-handling risks.
Performance is shared work
Server compilation removes browser transpilation. Route slimming reduces unnecessary payload. Static markup can help first paint when the component and deployment support it.
Those improvements do not make a theme fast by themselves.
An oversized hero, a late font stylesheet, invisible above-the-fold content, or a client effect that replaces a seeded product grid can undo the benefit. A bundled helper library can also increase JavaScript even when compilation happens on the server.
The kit provides check:perf, a fast static scanner for recurring source patterns. It is useful feedback, not a Lighthouse measurement or proof of accessibility. The published performance targets are goals to verify on the actual store.
For a theme, I care about appropriately sized images, responsive image selection, reserved media space, visible initial content, readable contrast, usable touch targets, and restrained motion. A floating purchase CTA should help someone complete an order without covering the form or stealing keyboard focus.
When a page receives server-seeded products through slot context, keep that initial content stable. Refetching and replacing it immediately wastes work and can shift the layout. When no seed exists, client fetching needs an honest loading and error state.
The important tests include a slow connection, an empty result, a missing image, a product without offers, and navigation between routes. A fast happy-path screenshot cannot answer those questions.
The CLI is part of the system
A document-based theme system needs good authoring tools. Otherwise developers end up editing long code strings and nested JSON by hand.
The CLI gives a theme a familiar local workflow:
npm install -g @feeef.dev/cli
feeef signin
feeef template init my-template --blank
cd my-template
feeef use
npm install
npm run build
npm run check
npm run devAuthentication uses browser OAuth with PKCE. The CLI is a public client; ordinary sign-in does not require a client secret. Store selection binds the project to a store through .feeefrc.
The package structure follows the document:
my-template/
feeef.template.json
schema.ts
props.json
design-system.md
pages/
home/
page.json
components/
hero.tsx
product/
components/
product-shell/
product-shell.tsx
slots/
shared/
components/
library/
components/
locales/
ar.json
en.json
fr.json
types/
scripts/
dist/Flat page components compile into sections.main. Folder-based slots and children become their matching document fields. Shared components support reuse inside the theme; library components can be offered as reusable editor blocks.
Commands such as these keep scaffolded structures consistent:
feeef template add page products
feeef template add component products grid --title "Product grid"
npm run typecheck
npm run check:perfThe source is what developers review and maintain. dist is compiler output. Editing the generated JSON and then rebuilding is a good way to lose a change.
Development previews use the real hosted shop
The default feeef template dev workflow watches the source, rebuilds it, and uploads a draft to the bound store. It does not start an independent local storefront application.
The draft lives under store.metadata.templatePreview; live template data has a separate location. A preview link establishes an httpOnly feeef_preview cookie so the developer can navigate the hosted shop under the draft. Customers outside that preview context continue to see the live theme. A preview banner makes the active mode visible.
That gets the author close to real catalog data and the real rendering environment. It also makes the loop dependent on network access and a matching API environment. A hosted production shop cannot read a draft uploaded only to an unrelated local backend.
Preview isolation applies to template selection. It is not a separate test store, and it does not imply that checkout, order creation, or every interaction uses fake data. Test commerce behavior with that distinction in mind.
The advantage is fidelity. The cost is less offline independence than a fully local application. The CLI includes a local path for storefront developers, but that is a separate workflow from the ordinary author preview.
Publishing and installing are different operations
A publish can update the marketplace listing, component records, translations, and an immutable semver release snapshot.
Installation is another action. The CLI exposes it through --apply:
# Publish a release; leave installation as a separate action.
feeef template publish --public --no-apply \
--version 1.0.0 \
--changelog "Initial release"
# Publish a release and install it on the bound store.
feeef template publish --public --apply \
--version 1.0.1 \
--changelog "Improve mobile product layout"Releases let merchants choose updates instead of receiving every new author build automatically. Paid templates add per-store licensing and marketplace distribution to the same model.
But “immutable release” and “all dependencies forever frozen” are different guarantees. Reference resolution and translation storage need to be considered alongside release data. The CLI also contains compatibility handling for older backends where release endpoints are unavailable. Behavior must be verified against the target backend rather than assumed from the newest local source.
There is another difficult problem: preserving merchant customizations across updates. Stable identifiers and separating props from code make that problem tractable. They do not implement a universal conflict-free merge.
If an author renames a prop, changes a slot, or removes a component the merchant customized, the update needs a migration policy. I do not want to claim every merchant edit survives every update simply because the document has stable IDs.
A responsible author treats schema changes as compatibility changes. Keep identities stable, add defaults carefully, explain breaking changes, and test an installation that has been edited by a merchant.
AI is another author of the same contract
AI-assisted generation is useful because the output has a defined home. It can propose a component's code, editable props, schema, and slots, or changes to a page structure.
That does not give generated output a special exemption from the rules. It still needs valid structure, stable identity, appropriate slot composition, the live-code policy, and the theme's visual contract.
The editing semantics matter especially here. If a response omits slot fields during an edit, the existing slot content should be preserved. An explicit empty slot is a deliberate value. Confusing omission with deletion can erase a merchant's composition during an unrelated change.
The kit includes documentation, types, and agent guidance so local coding assistants can work against the same architecture. The purpose is consistent output, not confidence in a model's assurances.
Generated code can parse correctly and still be wrong about shipping, currency, accessibility, or form state. Previewing and testing the result remain part of authoring.
Comparing Feeef with Shopify Liquid
I want this comparison to be fair, because Shopify solves many of the same merchant problems.
Modern Shopify themes already use JSON templates for section composition and settings. Merchants can add, remove, and reorder sections. Liquid implements rendering through its objects, tags, and filters. Describing Shopify as “only Liquid files” would miss its actual architecture. See Shopify's JSON template documentation and Liquid reference.
The meaningful difference is the implementation boundary. Shopify theme composition connects to Liquid implementations. Feeef composition connects to built-in React renderers or custom React code distributed through the template and component system.
| Concern | Shopify themes | Feeef templates |
|---|---|---|
| Composition | JSON templates and section/block configuration | TemplateData pages, sections, children, and named slots |
| Component implementation | Liquid with HTML, CSS, and JavaScript assets | Built-in React components or bundled custom React code |
| Merchant controls | Theme and section/block settings schemas | Template catalog and component propsSchema |
| Initial custom rendering | Liquid renders HTML through the platform | Compilation plus an environment-dependent isolated static-render path |
| Interactivity | JavaScript layered onto theme markup | Custom React executes against the injected browser scope |
| Distribution | Theme package managed by Shopify's platform | Compiled documents, component records, locales, and releases |
Liquid's constrained template language is an advantage when the goal is predictable server-rendered content with a carefully controlled execution surface. Its ecosystem and platform conventions also carry experience a newer system has to earn.
React offers familiar component composition and stateful interaction to developers who already work in that ecosystem. In Feeef, the same custom component body participates in the document-based system and the interactive runtime.
I accept greater complexity around JavaScript evaluation, browser payload, isolation, and SSR parity in exchange for that flexibility. A React authoring model does not automatically make a theme faster or more editable than a Liquid theme. The exposed schema, actual composition, and implementation quality decide that.
The Shopify facts above describe its documented model. The reasons I choose Feeef's boundaries are my engineering judgment, rather than a benchmark proving one platform wins every case.
Comparing other approaches
Shopify's Hydrogen is a React Router-based toolkit for custom storefronts, with Oxygen available for deployment. It belongs to the custom-application side of the spectrum. Feeef's template workflow keeps application hosting inside Lithium and distributes the theme as an artifact. Hydrogen's official getting-started guide describes its application workflow.
A standalone Next.js storefront offers freedom to define routes, server behavior, dependencies, and deployment. That freedom is valuable when a team needs to own the whole stack. A merchant editor, portable template package, update mechanism, and marketplace do not automatically arrive with that freedom; the application owner has to supply them.
WordPress block themes offer another useful comparison. Their templates use HTML files containing serialized block markup, and site-edited templates can live in the database and override theme files. The distinction between shipped defaults and user customization is central there too. WordPress documents that model in its template handbook.
A registry-only JSON page builder is narrower than Feeef's custom-code model: it can make execution easier to constrain, but its component catalog determines what can be expressed without extending the host. That is often a good trade. Not every product needs executable component bodies in its content model.
Feeef's position is a deliberate combination: structured, merchant-editable composition; developer-authored React capabilities; and a storefront runtime operated by the platform. It fits the problem I am trying to solve, with responsibilities that come directly from that choice.
The tradeoffs I want to keep visible
The architectural decisions are connected. Each useful property introduces work somewhere else.
| Decision | What it gives us | What we have to account for |
|---|---|---|
| Publish documents to a shared runtime | Installation without a separate theme server | Host API compatibility and platform constraints |
| Separate schema from instance data | Editable controls across Flutter and React | Keeping schema, defaults, and implementation aligned |
| Allow custom React code | Distinct components beyond the built-in catalog | Evaluation security, dependency size, and runtime errors |
| Compose with named slots | Merchant control inside meaningful boundaries | Context, nesting, layout, and server/client parity |
| Use library references | Shared implementations and lighter placements | Resolution failures, authorization, caching, and version semantics |
| Render in child processes | Separation from the main application's execution | Worker lifecycle, queueing, deployment support, and stronger isolation needs |
| Keep a compile-only fallback | Browser rendering when static rendering is unavailable | Empty custom first paint and reduced no-JavaScript coverage |
| Preview on a hosted shop | Real runtime and real catalog fidelity | Network dependency and real commerce side effects |
| Publish opt-in releases | A merchant can choose when to update | Migration and customization conflict handling |
| Record the visual contract | Consistency across contributors and AI edits | Keeping guidance matched to the actual interface |
The work ahead includes better custom SSR parity, stronger execution isolation, clearer dependency versioning, deliberate customization migrations, and fuller localization behavior. These are consequences of the architecture, and they deserve direct answers as the system develops.
What I want this system to give people
I want a developer to open a theme folder and feel able to build something with personality. Real code, useful types, ordinary source control, and room to design beyond a fixed block catalog.
I want a merchant to install that work and feel ownership of it. Their heading, their brand, their arrangement of fields, their decision about when to update. The structure should make everyday changes understandable.
And I want the shopper to see the result of those decisions as a clear, fast, accessible store. They should not have to know whether a component came from a library, a TSX file, or an editor.
The architecture has to carry that responsibility. A convenient compiler cannot excuse a broken checkout. A flexible component cannot excuse an uneditable heading. A server-rendering feature cannot excuse pretending that every component has the same first paint.
That is why I care about the small details: an empty slot that stays empty, an identifier that survives a save, a phone field that can move without rewriting submission, a cache key that includes the values being rendered.
Those details decide whether the freedom promised by the system reaches the person using it.
If you want to build with it, start with the template overview, the data model, and the CLI. Then open the blank kit, build a component, preview it on a store, and try changing it as a merchant would.
That is the introduction I wanted to write: the choices behind Feeef, the costs of those choices, and the kind of ownership I want the system to make possible.
Mohamed
Customers never see your draft
feeef template dev writes a second document on the store. I lost an hour once because my laptop and the shop were talking to different APIs.
Eight thousand stores, one process
How we measured Helium's origin performance, removed repeated render work, and kept the memory story honest.