Configuration Reference
Furin reads furin.config.ts (or .js / .mjs) at the project root. Every field is optional — the framework works out of the box with zero config.
import { defineConfig } from "@teyik0/furin/config";
import tailwind from "bun-plugin-tailwind";
export default defineConfig({
rootDir: ".",
pagesDir: "src/pages",
serverEntry: "src/server.ts",
clientLogging: false,
targets: ["bun"],
plugins: [tailwind],
bun: {
compile: "embed",
},
static: {
basePath: "/my-app",
outDir: "dist",
onSSR: "error",
},
});
rootDir
Type: string
Default: "."
Project root directory. All relative paths in the config (e.g. pagesDir, serverEntry) are resolved from here.
pagesDir
Type: string
Default: "src/pages"
Directory containing your page files. Furin scans this directory recursively and turns every .tsx file into a route.
serverEntry
Type: string
Default: "src/server.ts"
Path to the server entrypoint that creates the Elysia instance. Required for bun target builds. Not needed for static builds.
clientLogging
Type: boolean
Default: false
Inject the browser-side evlog logger into the generated hydration entry. Server-side request logging is unaffected by this option.
When disabled, Furin keeps evlog and evlog/http out of the browser bundle and uses a no-op logger for internal client events. Enable it when you want SPA navigation and custom browser events to be batched to /_furin/ingest and re-emitted server-side as service: "furin:browser".
Enabling it adds about 10 KB gzipped to the client bundle.
import { defineConfig } from "@teyik0/furin/config";
export default defineConfig({
clientLogging: true,
});
targets
Type: Array<"bun" | "node" | "vercel" | "cloudflare" | "static">
Default: ["bun"]
Build targets to produce. Only "bun" and "static" are fully implemented today. "node", "vercel", and "cloudflare" are planned.
| Target | Output | Use case |
|---|---|---|
bun | Server bundle + client assets | Self-hosted Bun runtime |
static | Pre-rendered HTML directory | GitHub Pages, Netlify, CDN |
You can also pass "all" to the CLI (--target all) to build every target defined in the config.
plugins
Type: BunPlugin[]
Bun plugins passed to the client bundler. Useful for Tailwind CSS, MDX, or any custom transform.
import { defineConfig } from "@teyik0/furin/config";
import tailwind from "bun-plugin-tailwind";
export default defineConfig({
plugins: [tailwind],
});
buildOnly flag
Plugins can be marked buildOnly: true to skip runtime registration. Useful for build-time-only transforms like Tailwind.
import type { FurinPlugin } from "@teyik0/furin/config";
const myPlugin: FurinPlugin = {
name: "my-plugin",
setup() { /* ... */ },
buildOnly: true,
};
bun.compile
Type: "server" | "embed"
Default: undefined (no compilation)
Compile the server to a native Bun binary.
| Mode | Behaviour |
|---|---|
"server" | Binary + client/ directory on disk |
"embed" | Self-contained binary with all assets in memory |
# CLI override
bunx furin build --compile embed
static
Configuration for the static build target.
static.basePath
Type: string
Default: ""
Sub-path prefix for static deployments (e.g. "/my-app" for user.github.io/my-app). Must start with / and have no trailing slash.
static.outDir
Type: string
Default: "dist"
Output directory for the static export.
static.onSSR
Type: "error" | "skip"
Default: "error"
Behaviour when SSR or ISR routes are encountered during a static build:
"error"— Throw at build time with the list of incompatible routes."skip"— Emit a warning and omit those routes from the output.
Dev plugins (bunfig.toml)
Dev-only plugins are configured in bunfig.toml, not furin.config.ts:
[serve.static]
plugins = ["bun-plugin-tailwind", "@teyik0/furin/strip-plugin"]
env = "FURIN_PUBLIC_*"
See the Plugins page for more details.
Runtime furin() options
The Elysia plugin also accepts runtime options in src/server.ts. These options affect how Furin is mounted in the application server, so they live next to your API plugin order rather than in furin.config.ts.
import { furin } from "@teyik0/furin";
import { Elysia } from "elysia";
import { api } from "./api";
import { sync } from "./sync";
new Elysia()
.use(await furin({ pagesDir: "./src/pages", sync }))
.use(api)
.listen(3000);
sync
Type: { adapter: SyncAdapter; principal: (ctx: Context) => string | Promise<string>; notifier?: SyncNotifier; streamPath?: string } | false
Enable Furin's built-in sync transport with an explicit runtime. Omit the option or pass false to disable the page-side transport. Furin mounts SSE at /_furin/sync, exposes cursor catch-up at /_furin/sync/changes, and injects the client runtime automatically. API mutations using furinSync() are synchronized by default and may opt out per route with sync: false.
principal is mandatory and must return the authenticated application identity. Furin calls it after upstream authentication hooks; replay keys are isolated by this value. Never derive it from an unverified cookie or header.
Synced mutations stay regular API routes. There are no server actions, generated mutation names, collections, or React wrapper providers required.
The runtime is mandatory whenever sync is enabled. SQLite :memory: is process-local and intended for development or tests; production rejects it. File-backed SQLite is valid for one machine, while multi-host replicas require a distributed adapter such as PostgreSQL, Redis, or a custom durable SyncAdapter. Use the same runtime object with furinSync(sync) and furin({ sync }); this keeps mutation reservation, replay, SSE, and catch-up on one namespace.
The notifier is optional. When omitted, Furin polls the adapter cursor. Notifications are only wake-ups: ordered recovery always reads adapter.readChanges(), including after a disconnect or lost notification.
The adapters are isolated, tree-shakable subpaths of @teyik0/furin:
@teyik0/furin/sync/postgres— recommended durable adapter usingBun.SQLtransactions and leases.@teyik0/furin/sync/redis— durable Redis Streams adapter plus an independent Redis notifier using Bun's nativeRedisClient.@teyik0/furin/sync/sqlite— process-local in memory or host-local on disk usingbun:sqlite.
Importing one subpath does not pull the other backends into the bundle.