🍡 mochi

SSR framework for Svelte 5 + Bun with islands-based selective hydration

On this page

Server islands with mochi:defer

Mark a component with mochi:defer to skip it during the initial SSR pass and render it on-demand from a dedicated endpoint after the page loads. Use this for personalized fragments (avatars, cart counts) that would otherwise block the surrounding HTML from being cached.

<!-- file: src/Page.svelte -->
<UserAvatar mochi:defer userId={123} />

Children of a deferred component become the fallback shown until the island resolves:

<UserAvatar mochi:defer userId={123}>
  <div class="skeleton">Loading...</div>
</UserAvatar>

Server island components are normal Svelte components with full access to the request context via getRequestContext() — cookies are forwarded automatically because the fetch is same-origin.

<!-- file: src/UserAvatar.svelte -->
<script>
  import { getRequestContext } from 'mochi-framework';
  const { cookies } = getRequestContext();
  const session = cookies.get('session');
</script>

<p>Welcome back, {userName}!</p>

Fetch flow

  1. SSR emits a <mochi-server-island> custom element holding the fallback content; the component itself is not rendered.
  2. Props are serialized with devalue, HMAC-signed, and stamped onto the element as signed-props.
  3. On connectedCallback, the element fetches /_mochi/island/{ComponentName}?props={signedProps} (the /_mochi prefix follows assetPrefix).
  4. The server verifies the signature, decodes the props, renders the component, and returns the HTML.
  5. The HTML replaces the fallback inside the custom element.

Failed fetches are retried with exponential backoff (default 5 retries, 1s–10s); pass mochi:defer={{ retries: 10 }} to override.

Combining with hydration

Apply mochi:hydrate alongside mochi:defer to fetch the island on-demand and then hydrate it for client-side interactivity:

<ShoppingCart mochi:defer mochi:hydrate items={initialItems} />

Lazy server islands with mochi:defer:visible

Defer the fetch until the wrapper scrolls into view, mirroring mochi:hydrate:visible:

<UserAvatar mochi:defer:visible userId={123}>
  <div class="skeleton">Loading...</div>
</UserAvatar>

<UserAvatar mochi:defer:visible={{ rootMargin: '200px' }} userId={123} />

Pass rootMargin to start fetching before the island enters the viewport. rootMargin and retries can be combined: mochi:defer:visible={{ rootMargin: '200px', retries: 10 }}. Combinable with mochi:hydrate / mochi:hydrate:visible for interactive lazy islands.

Provide fallback children when using :visible so the user has something to scroll past while waiting.

Props

Props are serialized with devalue — see Passing props to islands for the full list of supported types. Server islands additionally HMAC-sign the payload and pass it as a query parameter; if the signed props exceed URL length limits (~1800 bytes), a warning is emitted.

Do NOT ship large blobs through server-island props; instead, fetch the data inside the component using getRequestContext(). Signed-prop URLs over 1800 chars trigger a runtime warning.

Signing key

Props are signed with a 32-byte key resolved at startup from process.env.MOCHI_KEY (base64url-encoded). If MOCHI_KEY is unset, Mochi generates a random key and logs a warning — fine for local dev, broken across restarts and multi-instance deploys.

# .env
MOCHI_KEY=<base64url-encoded 32-byte secret>

Generate one and write it to .env with mochi-framework generate-key:

bunx mochi-framework generate-key

Do NOT commit MOCHI_KEY to version control; instead, supply it through your platform’s secret store. Generate one with openssl rand -base64 32 | tr '+/' '-_' | tr -d '='.