stoneware

Documentation

What it solves

The specific problems Stoneware exists for — and the ones it does not.

The web ships a lot of JavaScript to sites that do not need it. Stoneware makes HTML the default and JavaScript the exception.

           Mostly a document?
                   │
                   ▼
              Server HTML          ──►  0 KB client JS
                   │
           Need interaction here?
                   │
                   ▼
                Island            ──►  small, local client JS
the whole decision

Stoneware is not a general-purpose replacement for anything. It was built for one shape of project: content-heavy, SEO-sensitive sites with a handful of interactive widgets. Within that shape, these are the problems it takes seriously.

1. You ship a runtime to render a document

A blog post, a pricing page, a docs page: the content is fully known before the response finishes. Sending a framework runtime so the browser can reconstruct it is work nobody asked for, paid on every visit, on hardware you do not control.

In Stoneware a page with no islands ships no JavaScript. Not a small runtime, not a hydration shim — zero bytes, no script tag at all. That is asserted by the test suite, not just intended.

  Whole client runtime (signals + hydrate + DOM)     ~3.4 KB
  One island, e.g. the counter on the home page       ~0.2 KB
  A page with no islands                                   0 B

  Runtime dependencies                                       1
  (@preact/signals-core — everything else is Bun's own APIs)
measured on this site's own production build, gzipped

2. The client/server boundary drifts

When the boundary is a directive that propagates through the import graph, it moves without being moved. One innocent import pulls a subtree to the client, and you discover it from a bundle analyzer weeks later.

Stoneware puts the boundary on the filesystem. Files under islands/ are bundler entry points; files under routes/ are never handed to the bundler at all. Server-only code does not stay server-only because a heuristic classified it correctly — it stays server-only because nothing can carry it across.

You cannot accidentally make a page interactive. There is no directive to forget, and no import that quietly changes where code runs.

3. Hydrating things that will never change

Take a twenty-page company site where the only interactive parts are a newsletter form and a pricing calculator. A client-heavy architecture still asks the browser to boot a runtime and walk the whole page before any of it is live.

  Header        ──►  no
  Article       ──►  no
  SEO content   ──►  no
  Footer        ──►  no

  Newsletter    ──►  yes      islands/Newsletter.tsx
  Calculator    ──►  yes      islands/Calculator.tsx
what actually needs to hydrate

The header is the same markup on every page and will never change after it is painted. Stoneware does not hydrate it, because there is nothing there to hydrate — it is a string the server produced, and it stays that way.

4. The component model costs more than it returns here

Hook ordering rules, dependency arrays, stale closures, and memoization as a standing performance tax are a reasonable trade for a complex application UI. For a page that renders once and then mostly sits there, it is overhead with no matching benefit.

A Stoneware template is a plain function, called once per request, on the server. Nothing to memoize, no render loop to reason about, no rules-of-hooks lint plugin. Reactivity exists only inside islands, where it earns its keep — and an update writes to one text node rather than re-running a tree.

5. Security is opt-in, and CSP is what everyone skips

CSRF protection is usually middleware you have to remember on the right routes. Content-Security-Policy is usually absent, because adopting a strict one means fighting a framework that emits inline scripts and styles — so you end up with unsafe-inline, nonce plumbing, or nothing.

  • Escaping happens in the renderer. There is no global switch to turn it off.
  • CSRF is verified in the request pipeline before any handler runs, on every mutating request. A raw <form> does not skip protection — it fails.
  • A strict CSP ships by default and works, because Stoneware never emits inline executable script. No nonces to plumb.
  • A production build refuses to start without a CSRF secret rather than falling back to something that appears to work.
This documentation site runs under that default policy with no overrides. If any page here needed an exception, that would be a bug in the framework rather than in the page.

6. Hydration mismatches

Reconciling a client tree against server markup produces a whole category of bug that only appears in production, usually as a warning nobody can reproduce. Stoneware does not reconcile: it builds the island's tree and replaces the marked element outright. There is nothing to mismatch.

The honest trade: this is a replacement, not a resumption. It happens as the island's bundle loads, before anyone has interacted with it, so in practice nothing is lost — but DOM state inside an island is not carried across the swap.

7. Toolchain sprawl

A conventional setup accumulates a bundler, a dev server, a TS runner, dotenv, a CSRF library, a test runner, and the glue holding them together — each with its own config file and release cadence.

Stoneware has one runtime dependency. Serving, bundling, escaping, CSRF tokens, routing, .env loading, and the test runner are all Bun's own APIs. That is a deliberate constraint, not a coincidence: if Bun ships it, Stoneware does not add a package that reimplements it.

What that looks like on a real site

A company site: twenty pages, fifty blog posts, SEO metadata throughout, a contact form, a newsletter signup, and one pricing calculator. Almost all of it is a document. Three parts are not.

  20 pages + 50 posts   ──►  routes/**            server-rendered HTML, 0 KB JS
  SEO metadata          ──►  routes/**            rendered, never hydrated

  contact form          ──►  routes/api/*.ts      server action, CSRF verified
  newsletter            ──►  islands/*.tsx        small island
  calculator            ──►  islands/*.tsx        small island

  everything else       ──►                       0 KB client JavaScript
where each piece ends up

The contact form does not even need an island: a <Form> posting to a server action works with JavaScript disabled, and the CSRF token is injected for you. An island is only worth it when you want the interaction to happen without a page load.

What it does not solve

Worth stating plainly, because a framework that claims everything is useful for nothing. Stoneware has nothing to say about databases, authentication, offline support, realtime collaboration, or large-scale client state. It is a rendering and routing layer with sensible security defaults, and that is the whole of it.

It also does not make a site automatically fast or automatically secure. It removes a category of framework-level mistake — unescaped interpolation, a forgotten CSRF check, an absent CSP, JavaScript shipped for no reason. Your own queries, your own auth, and your own payload sizes remain yours.

When not to use it

The fastest way to be disappointed by Stoneware is to bring it to a problem it was not built for.

  • Genuinely app-like UI — a dashboard, an editor, anything with heavy shared client state. Use a SPA framework; that is what they are good at.
  • You need client-side routing. Stoneware does full page loads.
  • You need streaming SSR, resumability, or partial rendering. Deliberately deferred for v0.1.
  • You are not on Bun. Stoneware is Bun-native by design, not Node-compatible-via-Bun.
  • You need a large plugin ecosystem. This is v0.1; there isn't one.

If your page is mostly a document with a few live parts, the trades above are nearly all upside. If it is mostly an application, almost none of them are.

Something wrong in the framework itself rather than the page? Open an issue on GitHub.