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. On the client, Mochi replaces these files with stubs that throw if used. The real module compiles for SSR only.
// 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 whole convention — no runtime API, no config. Import with the extension (./db.server.ts). Extensionless ./db.server also works.
Types are free
A type-only import is erased before the client build resolves anything, so a .server.ts file is also the right home for the types describing its data — even for types used inside a hydratable island.
// db.server.ts
export interface Row {
id: number;
title: string;
}
export const listRows = (): Row[] => db.query('SELECT * FROM rows').all() as Row[];<!-- RowList.svelte — hydratable island -->
<script lang="ts">
import type { Row } from './db.server.ts';
let { rows }: { rows: Row[] } = $props();
</script>Mochi stubs only value imports. Use import type (or import { type Row }) so the compiler drops the import instead of resolving it to a stub.
What gets stubbed
On the client, every export of a .server.ts file throws on any use. The error names the export and its origin file:
getVersion from /…/db.server.ts was called on the client; this is a server-only export.Server-only components
Since: 0.10.0 (not released yet): Server-only components (*.server.svelte) ship in the next Mochi release (0.10.0). This section describes the upcoming API.
Name a component *.server.svelte to keep it SSR-only. It renders on the server like any component, but the client build replaces it with a stub, so it never ships to the browser — even when an island pulls it in through a barrel re-export.
<!-- Changelog.server.svelte — rendered on the server, stripped from client bundles -->
<script lang="ts">
import { readFileSync } from 'node:fs';
const entries = readFileSync('CHANGELOG.md', 'utf8');
</script>
<pre>{entries}</pre>Import it with the extension (./Changelog.server.svelte). The framework’s own ViewTransitions and RawScript use this convention.
Only the default (component) export is stubbed. For server-only values, use a .server.ts file.
Unsupported
export * from './x'— Mochi warns at build time. Declare named exports in the.server.tsfile directly.
See it in action
Live demos showing key concepts from this page