🍡 mochi

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

On this page

Server-only imports

Any module reachable from a hydratable island gets bundled into the client. To use a server-only library (bun:sqlite, node:fs, anything that touches the filesystem) from inside an island, put the library plus a thin wrapper in a *.server.ts file. Mochi replaces these files with throwing-Proxy stubs on the client; the real module is only compiled for SSR.

// db.server.ts
import { Database } from 'bun:sqlite';

const db = new Database(':memory:');

export const getVersion = (): string => (db.query('SELECT sqlite_version() as v').get() as { v: string }).v;
<!-- FactCard.svelte — hydratable island -->
<script lang="ts">
  import { hydratable } from 'svelte';
  import { getVersion } from './db.server.ts';

  const version = await hydratable('app:sqlite-version', () => getVersion());
</script>

<p>SQLite {version}</p>

The .server.ts (or .server.js) suffix is the entire convention — no runtime API to call, no config. Import with the extension (./db.server.ts); extensionless ./db.server also works. Types follow the import normally.

What gets stubbed

Every named and default export of a .server.ts file is replaced with a Proxy that throws on both function calls and property access. The error message names the export and its origin file so a stray client-side use surfaces cleanly:

getVersion from /…/db.server.ts was called on the client; this is a server-only export.

Not supported

  • export * from './x' — Mochi warns at build time; declare named exports in the .server.ts file directly.
  • .server.svelte — component-level convention is not provided. Put server-only code in plain TS and call it from a component.