Menu
AkurAI-Build
publicLatest change 427bed429da42fe5a2653bd512f954c9c806ac2b - docs: satisfy README markdown lint by Olafur Bui
# 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):
```sh
cargo build --release --locked
```
Run the native MiniJinja/vector application:
```sh
./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:
```sh
./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` |
<!-- markdownlint-disable MD013 -->
| 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. |
<!-- markdownlint-enable MD013 -->
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:
```json
{
"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 |
<!-- markdownlint-enable MD013 -->
`.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:
<!-- markdownlint-disable MD013 -->
| 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 |
<!-- markdownlint-enable MD013 -->
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:
```json
{"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>`:
```text
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:
```sh
./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:
```sh
./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:3100` is 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 `Origin` must match the configured public origin. Without one, the
Host-header fallback applies only to loopback listeners running
`bunfork dev` (development mode). Origin-bearing mutations are otherwise
rejected, including loopback `bunfork serve` binds behind a same-host
reverse proxy, where the proxy typically forwards the client-supplied
`Host` and 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](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](docs/architecture-review)
and [docs/static-schema-evolution.md](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](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](LICENSE) and
[THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).