Menu
AkurAI-Build
public427bed4
bunfork
bunfork is a Linux-first Rust web runtime with one CLI for verified static
artifacts, server-rendered MiniJinja pages, SQLCipher migrations/backups, and
bounded exhaustive vector search.
It hosts prebuilt static output from documented Svelte, SvelteKit, Next.js, Vue, Nuxt, and Vite configurations. It is not source-, build-, adapter-, or server-runtime-compatible with those frameworks: Bunfork does not embed JavaScript, Node, V8, JavaScriptCore, RSC, Nitro, or a framework compiler. Browser JavaScript in an admitted artifact is served byte-for-byte and runs only in the browser.
The default listener is 0.0.0.0:3100; port 3000 is never used by default.
Quick starts
Build Bunfork (Rust 1.89 or newer, edition 2024):
cargo build --release --locked
Run the native MiniJinja/vector application:
./target/release/bunfork keygen
./target/release/bunfork keygen --out .bunfork.token
./target/release/bunfork migrate
./target/release/bunfork dev
The database key and API token must decode to different 32-byte secrets; case-only variations of the same hexadecimal key are rejected.
Run a prebuilt static export with no database, key, token, migration, tenant, or model state:
./target/release/bunfork admit \
--artifact frontend/dist \
--manifest bunfork-static.json \
--mode spa \
--fallback index.html \
--trailing-slash ignore
./target/release/bunfork doctor \
--static frontend/dist \
--manifest bunfork-static.json
./target/release/bunfork serve \
--static frontend/dist \
--manifest bunfork-static.json
One CLI
Global data options precede the command:
| Option | Environment | Default |
|---|---|---|
--database <PATH> | BUNFORK_DB | data/bunfork.db |
--key-file <PATH> | BUNFORK_KEY_FILE | .bunfork.key |
--tenant <ID> | BUNFORK_TENANT | default |
--model <ID> | BUNFORK_MODEL | default |
| Command | Purpose |
|---|---|
keygen [--out <PATH>] | Create a private 256-bit secret. |
build --run <PROGRAM> --arg <ARG>... --output <DIR> --manifest <FILE> [artifact options] [--json] | Run one explicit argv build command with a reduced environment, then admit its output. No shell or framework detection is inserted. |
admit --artifact <DIR> --manifest <FILE> [--mode mpa|spa] [--base /] [--fallback FILE] [--trailing-slash ...] [--json] | Scan a completed static directory and atomically write its deterministic manifest. |
doctor --static <DIR> --manifest <FILE> [--json] | Verify every file, byte length, SHA-256 digest, and manifest invariant offline. Exit code 3 means verification failed; the report names the first divergent file. |
dev [native server options] | Rescan native pages and re-read public assets per request. This is reload, not HMR. |
dev --static <DIR> --manifest <FILE> | Serve a frozen artifact without data state; rebuild/restart to see changes. |
serve [native server options] | Serve native pages and vector APIs. start is an alias. |
serve --static <DIR> --manifest <FILE> | Verify, preload, and serve a static artifact without opening SQLCipher. |
migrate | Apply embedded migrations transactionally and verify the exact schema. |
backup [--out <PATH>] [--json] | Create, publish, and verify an encrypted no-overwrite backup. |
restore <BACKUP> --yes | Verify and restore a backup while preserving the previous live database. |
vector put|search|delete ... | Manage tenant/model-scoped encrypted vectors. |
test [--pages <DIR>] [--public <DIR>] | Run toolchain-free route, migration, vector, and backup smoke checks against a throwaway encrypted database. cargo test runs in CI and during deploy. |
deploy [--out <DIR>] [--force] | Build a digest-owned native bundle; default output is bunfork-dist. |
deploy --artifact <DIR> --static-manifest <FILE> [--out <DIR>] [--force] | Build a digest-owned static bundle with the Bunfork binary and systemd units. |
Secret files take precedence over BUNFORK_DB_KEY and BUNFORK_API_TOKEN
environment values.
bunfork-static-v1
Admission accepts an immutable directory of ordinary browser files and writes a strict manifest:
{
"schema": "bunfork-static-v1",
"root": ".",
"mode": "mpa",
"base": "/",
"fallback": null,
"trailing_slash": "directory",
"precompressed": true,
"immutable": true,
"files": {
"index.html": {
"sha256": "…64 lowercase hexadecimal characters…",
"bytes": 1234
}
}
}
Unknown/duplicate fields, extra/missing/mutated files, symlinks, hard links, special files, hidden/sensitive paths, malformed URLs, and traversal fail closed. Scans are bounded to 50,000 files, 256 MiB per file, and 512 MiB total. Serving preloads the entire verified artifact after re-hashing, so admission and serving share one bound and post-bind filesystem swaps are eliminated.
Static HTTP behavior includes base paths, directory routes, .html exports,
MPA 404 pages, explicit SPA fallbacks, asset/API fallback exclusions,
GET/HEAD/405, ETags/304, conservative document caching, immutable
caching only for semantic SvelteKit/Next namespaces, and quality-aware
.br/.gz negotiation. Uncertain Vite/Nuxt/custom assets revalidate instead
of relying on filename guesses. trailing_slash selects the canonical URL
shape: directory redirects /guide to /guide/, redirect canonicalizes
/guide/ to /guide, and ignore serves both without redirects.
/_bunfork/health and /_bunfork/ready are reserved.
Producer configurations:
<!-- markdownlint-disable MD013 -->| Producer | Admitted output | Bunfork mode |
|---|---|---|
| Svelte or Vue with Vite | vite build output, normally dist | spa --fallback index.html --trailing-slash ignore for a client router; otherwise mpa |
| SvelteKit | adapter-static output, normally build | mpa; use spa only with an explicit adapter fallback |
| Next.js | output: 'export' output, normally out; trailingSlash: true is recommended | mpa --trailing-slash directory |
| Nuxt | nuxt generate / prerendered .output/public | mpa, or explicit spa for a client-only fallback export |
.next/server, Next standalone, SvelteKit adapter-node, Vite SSR, and
Nuxt/Nitro server output are rejected scope, not partially emulated features.
Native pages and assets
Only .html files below app/pages become routes:
| File | Route |
|---|---|
index.html | / |
about.html | /about |
notes/[slug].html | /notes/:slug |
docs/[...path].html | required terminal catch-all |
docs/[[...path]].html | optional terminal catch-all |
(site)/about.html | /about; route groups do not enter the URL |
Repeated parameters, ambiguous routes, invalid templates, hidden entries, links, and special files fail startup/deploy. Production pages are compiled and loaded before binding, so later path swaps cannot expose files.
Templates in app/templates support MiniJinja inheritance/includes/macros;
the sample uses base.html. _404.html and _500.html are optional error
pages. Pages are registered into the same MiniJinja environment under
generated page:<pattern> names. A shared template whose path resolves to one
of those generated names fails startup with the colliding name rather than
being silently overwritten. Pages receive auto-escaped params, bounded
multi-value query, pathname, tenant, and model; the included
/notes/:slug demo visualizes the matched path, extracted slug, and repeated
query values. Files in public are validated, hashed, and preloaded before
bind, then served below /assets without request-time filesystem reads.
Vector API
GET /api/health, /api/ready, /_bunfork/health, and
/_bunfork/ready are public operational probes. The two health aliases
identify the running service and build, with whole-second uptime:
{"status":"ok","service":"bunfork","version":"...","uptime_seconds":42}
The ready aliases return {"message":"ok"} after startup. Native readiness
checks the open encrypted database and returns HTTP 503 with
{"message":"unavailable"} when that check fails; static readiness has no
database dependency. Vector endpoints require
Authorization: Bearer <API_TOKEN>:
PUT /api/vectors/note-1
{"id":"note-1","content":"hello","embedding":[1,0,0]}
POST /api/vectors/search
{"embedding":[1,0,0],"limit":5}
DELETE /api/vectors/note-1
Invalid identifiers, dimensions, norms, limits, and budgets are client errors
rather than HTTP 500s. The HTTP 400 body carries the descriptive validation
message (for example, which capacity or budget was reached), not a generic
string. The exhaustive cosine-similarity scan is bounded to 4,096 dimensions,
10,000 candidates, 10,000,000 multiply-adds, 25 results, and 64 KiB content.
JSON bodies are capped at 1 MiB. Requests time out after 30 seconds with HTTP
408. Each listener serves at most 1,024 concurrent requests and 1,024
concurrent connections, and a matching header-read timeout closes slow-header
or idle keep-alive sockets. Set BUNFORK_REQUEST_TIMEOUT_MS to tune that
deadline. Bunfork does not load SQLite extensions or claim ANN/full-text
search.
Encrypted data and recovery
SQLite is encrypted with bundled SQLCipher. Migrations are embedded and transactional; validation checks application/user versions, the exact migration ledger, exact table/index SQL, and rejects unexpected schema objects.
Backups use the same key as the live database; they are not key rotation. Backup/restore pin file identity with no-follow descriptors, reject links, sync publication, verify the exact encrypted schema, and preserve a timestamped pre-restore database. Stop the server before restore. Same-UID hostile races remain outside the filesystem guarantee; isolate untrusted code under another OS account/container.
Deployment
deploy tests, release-builds, stages, hashes, fsyncs, and atomically installs
bunfork-dist. bunfork.json uses the bunfork-deployment-v2 schema and
covers every bundled file except itself. --force validates that complete
inventory before replacement. Hidden/protected outputs—including .git,
.refrepos, app, src, data, backups, deploy, migrations, public,
and target—are always refused. The prior verified release is preserved as a
sortable bunfork-dist.previous-<unix-timestamp>-<pid> sibling and never
automatically deleted; prune old siblings manually when disk space matters.
Native runtime state belongs outside the release:
./bunfork-dist/bunfork \
--database "$HOME/.local/share/bunfork/bunfork.db" \
--key-file "$HOME/.config/bunfork/db.key" \
serve --token-file "$HOME/.config/bunfork/api.token"
Static bundles run from any working directory:
./bunfork-dist/bunfork serve \
--static ./bunfork-dist/site \
--manifest ./bunfork-dist/bunfork-static.json
The included native/static user units use %h/.local/lib/bunfork,
0.0.0.0:3100, and systemd filesystem/process hardening. Bunfork does not
provision hosts, upload releases, configure DNS/TLS, or delete prior releases.
Security boundaries
0.0.0.0:3100is intentionally LAN-visible. Firewall it and use a trusted TLS reverse proxy before internet exposure; Bunfork has no built-in TLS.- Bearer and decoded-secret comparisons are constant-time. Browser mutations
with
Originmust match the configured public origin. Without one, the Host-header fallback applies only to loopback listeners runningbunfork dev(development mode). Origin-bearing mutations are otherwise rejected, including loopbackbunfork servebinds behind a same-host reverse proxy, where the proxy typically forwards the client-suppliedHostand both headers are attacker-supplied end-to-end. The bearer token remains primary API authorization. - Native CSP is strict. Static CSP permits same-origin code plus inline scripts/styles because official SvelteKit/Next exports require bootstrap inline code. Use a reverse proxy with build-specific CSP hashes/nonces for a stricter policy.
- Static mode has no data/secret dependency. A server-side foreign runtime is not supervised because a same-UID process could read Bunfork keys/database regardless of environment filtering.
- Encryption does not protect a compromised live process, service account, root user, TLS proxy, binary, or unlocked key. There are no user accounts, roles, distributed locks, remote backup transport, or Windows hardening guarantees.
Verification and references
Ten read-only Codex CLI red-team passes and the final no-ship checklist are preserved in docs/adversarial. The P0 findings drove digest-owned deploys, secret byte comparison, descriptor-pinned recovery, strict artifact admission, and listener-level regression tests.
A second ten-architect architecture review, its per-commit remediation map,
the consciously deferred backlog, and the bunfork-static-v1 schema evolution
policy are preserved in docs/architecture-review
and docs/static-schema-evolution.md.
Committed listener-level tests start the real binary in both static and native modes. Native coverage includes pages/assets, probes, authentication/origin checks, vector mutations/search, and clean SIGTERM shutdown. Exact package versions and live results for official Svelte/Vite, SvelteKit, Next, Vue/Vite, and Nuxt builds are recorded in docs/framework-e2e.md.
Pinned research checkouts under .refrepos/ are excluded from
compilation/deployment:
| Repository | Commit | Behavior studied |
|---|---|---|
| Bun | aaaa53f1 | compact CLI/runtime boundaries |
| Deno | c537e012 | artifact/runtime distinctions and shutdown |
| Svelte | b4d1583ae20f | compiled browser artifact boundary |
| SvelteKit | 5c38e515db7f | adapter-static, fallbacks, routes |
| Next.js | 1bd2fd585aac | export/standalone split and static routing |
| Vue | c0606e91798c | SFC compiler versus browser output |
| Nuxt | 33f0fdb9d208 | generated public versus Nitro output |
| Vite | c961cae2868c | static output, base paths, hashed assets |
| Rari | 373fa706 | Rust routing versus V8/RSC boundary |
| Leptos | f81ffcd5 | request-scoped Rust SSR patterns |
| Dioxus | e1c63426 | deterministic routes and full-stack boundaries |
| Andromeda | 0ef48722 | Rust runtime and SQLite boundaries |
| Ponytail | 14a0d795 | minimal implementation discipline |
Bunfork is MIT licensed; see LICENSE and THIRD_PARTY_NOTICES.md.