🍡 mochi
An experimental SSR framework for Svelte 5 and Bun.
Render everything on the server; ship JavaScript only where it earns its place.
What is Mochi?
Mochi is a lightweight, server-first framework for Svelte 5 on Bun. Mochi websites render server-side on every request and ship as plain HTML. Components only ship JavaScript when you explicitly mark them as islands.
Quick start
Documentation
Setup, hydration modes, routes, hooks, forms, cookies — everything in one place.
Start reading →Demos
Each demo lives on its own page. Pick one below to see the feature in isolation.
Basic
Hello World
How server-side rendering works — a Mochi.page() renders Svelte on the server and ships zero JavaScript.Hydration Modes
How the hydration modes work — mochi:hydrate, mochi:hydrate:visible, rootMargin tuning, and mochi:defer server islands side by side.Hydratable
How hydratable() works — compute a value once on the server and reuse it on the client instead of re-running async work during hydration.isHydratable()
How isHydratable() works — detect from any depth whether the current subtree will hydrate, for SSR-only fallbacks without prop forwarding.View Transitions
How view transitions work — drop <ViewTransitions /> into a layout to animate full-page navigations with zero JavaScript.Custom Transitions
How custom view transitions work — supply your own @keyframes to <ViewTransitions /> via custom={{ in, out }}.Shared State
How shared state across islands works — two separate islands driving the same reactive $state.Server Islands
How server islands work — components marked mochi:defer render server-side on demand after the initial page is delivered.Crossing the server-client boundary with props
How props cross the server-client boundary — Date, Map, Set, BigInt, URL, typed arrays, and even cyclic refs survive devalue's round-trip into a hydrated island.Client-only Islands
How client-only islands work — components marked mochi:clientOnly skip SSR and mount in the browser, with a fallback snippet until then.Lazy Islands
How lazy hydration works — islands marked mochi:hydrate:visible hydrate and load their CSS only when scrolled into view.Lazy Server Islands
How lazy server islands work — server islands marked mochi:defer:visible only fetch when the wrapper scrolls into view.Font loading
How font loading works — ship fonts via @fontsource packages or standalone .woff2 files, automatically bundled and linked from the page head.MdSvex
How mdsvex works — a .md file compiled through mdsvex and rendered as a Svelte component, embedded <script> and all.Nested Components
How whole-subtree hydration works — a five-level recursive tree where hydrating the root carries the entire subtree in one island.Nested Islands
How nested islands work — a mochi:defer server island wrapping mochi:hydrate components, and server islands nesting more server islands.Nested Island Max Depth
How deeply nested server islands work — a four-level mochi:defer chain where each level fetches the next on demand and the prebuild precompiles the whole chain.Shared Props
How island prop deduplication works — nine islands share three unique payloads, each serialized once and referenced via props-ref.Unique IDs
How stable island IDs work — Svelte's native $props.id() gives SSR-consistent, per-instance ids, namespaced inside server islands.HTML Entities in Props
How HTML entities in island props work — an entity in a static prop (label="Tom & Jerry") decodes identically on the server and after hydration.Runed Utilities
How third-party Svelte 5 libraries run in islands — Runed's reactive utilities (Debounced, StateHistory, PersistedState, PressedKeys, AnimationFrames, FiniteStateMachine, resource…) hydrated inside Mochi.Data & serialization
Server Props
How server props work — pass fresh per-request data into a page via serverProps on Mochi.page().Data Loading
How server-side data loading works — fetch on the server, cache with MochiCache, and render at request time.Cookies
How cookies work — read and write on the server and the client through one MochiCookieJar API (cookies.get/set/delete).Isomorphic URL
How the isomorphic URL helper works — one import that reads the request URL on the server and window.location on the client.Cache Events
How cache events work — subscribe to MochiCache lifecycle events (hit, miss, set, evict) through mochiEvents for observability.Request Cache
How the request cache works — requestMemo and requestCache run an expensive computation once per request no matter how many components call it, then discard it at the request boundary.Image: Component
How the <Image> component works — named sizes, ThumbHash blur-up placeholders, and encrypted deferred URLs whose transforms run lazily on the endpoint.Image: Invalidation
How image invalidation works — invalidateImage() hard-evicts the shared original so every named size re-fetches in lockstep.Image: Named sizes
How the image transform pipeline works — declare resize / rotate / flip / modulate / format as named sizes; getImageUrl mints a deferred URL, getImage runs one inline.Image: Events
How image events work — subscribe to image:store / image:delete on mochiEvents to mirror the <Image> cache to durable storage like S3.Request ID
How request IDs work — every request gets a UUID v7 on getRequestContext().requestId that rides every lifecycle event for correlation.Cookie Vary Test
How cookie-partitioned caching works — a page that sets Vary: Cookie so responses key on cookies.Portable Text
How Portable Text rendering works — @portabletext/svelte maps a JSON block array onto your own Svelte components for types, marks, block styles and lists.Endpoints & realtime
Real-time Chat
How WebSocket routes work — a hydrated island over Mochi.ws() with pub/sub broadcast and in-memory history.API Endpoints
How API routes work — define JSON endpoints with Mochi.api(), tested live against the running server.Rate Limiting
How rate limiting works — a rateLimit config on the route caps requests per IP per minute and serves the 429 error page past the limit.File Routes
How file routes work — serve a file from disk with Mochi.file(), as a static path or a per-request resolver.Real-time Streams
How server-sent events and WebSocket streaming work — live SSE and WebSocket clocks, lazily hydrated via mochi:hydrate:visible.Background jobs with queues
How background job queues work — offload work to a Mochi.queue() with an embedded worker, no Redis.Forms
Form Actions
How form actions work — a form rendered twice, as a plain HTML POST and intercepted with {@attach enhance(...)}.Send Email
How sending email works — dispatch through Mochi.email() and read it back in the /_mochi/email dev outbox.Using form return data
How form action return data works — an action returns success({...}); {@attach enhance(...)} updates the UI in place, plain HTML re-renders.Form Errors
How form action errors work — a thrown action error shows inline via {@attach enhance(...)}, or as the Mochi error page on a plain submit.Form Redirects
How form action redirects work — redirect(303, …) is intercepted as a JSON envelope by {@attach enhance(...)} or followed natively by the browser.File Uploads via form actions
How file uploads through form actions work — multipart/form-data validated with fail() and success(), shown enhanced and plain.Reloading associated form data
How reloading associated data works — after a successful submit, refetch the related list inside enhance(), or rely on the post-POST re-render.Captcha
How the captcha works — slide-to-verify backed by a hash chain and proof-of-work, with no third party and no tracking.Captcha Styling
How captcha theming works — the same captcha four ways, every colour a CSS custom property with a built-in fallback.Cancelling form submissions
How cancelling form submissions works — cancel() stops the fetch before it fires; controller.abort() stops one mid-flight.