Polyglot, file-system-routed full-stack framework for Bun.
Tachyon lets you define backend handlers as exported functions in any language, build reactive front-end pages with Tac companion classes and decorators, prerender them into static HTML, and preview the bundled output locally without extra setup.
Requires Bun v1.3 or later.
The fastest way to start is to scaffold an app and use the generated scripts:
In a scaffolded Tachyon app, these are the recommended development commands:
Use yon.init to generate a deployable starter app with pages, components, entry script, package scripts, and Amplify config.
Generated starters include browser/pages/index.html, browser/components/hero.html, server/routes/GET.js, package.json, amplify.yml, and the standard bundle / preview / serve scripts.
Routes map directly to the filesystem. Each directory is a route segment; each file is an HTTP method.
Handler files use their language extension (GET.js, POST.ts, PATCH.rs). The filename stem must be an uppercase HTTP method. Dynamic route segments use _slug on disk and become :slug at runtime. The first path segment cannot be dynamic, adjacent dynamic segments are rejected.
Every route file exports a handler(request) function. The framework calls it with a request object and serializes the returned value as the response.
export async function handler(request) {
return {
message: "Hello, " + request.body.name,
version: request.paths.version,
page: request.query.page
}
}The request object contains:
Handlers can also be class-based: export a default class named after the HTTP method with a handler(request) method. Class-based handlers receive the same request object and return plain data the same way.
Route files use their language extension — GET.js, POST.ts, PATCH.py, GET.go, DELETE.rs, and so on. Every language exposes the same handler(request) contract. Dynamic languages are invoked directly; compiled languages get tiny generated wrappers — no third-party adapter dependencies.
| Language | Handler signature | Runtime |
|---|---|---|
| JavaScript | <code>export function handler(request)</code> | Dynamic |
| TypeScript | <code>export function handler(request)</code> | Dynamic |
| Python | <code>def handler(request)</code> | Dynamic |
| Ruby | <code>def handler(request)</code> | Dynamic |
| PHP | <code>function handler($request)</code> | Dynamic |
| Dart | <code>handler(Map<String, dynamic> request)</code> | Dynamic |
| Go | <code>func Handler(request map[string]any) any</code> | Compiled |
| Rust | <code>pub fn handler(request: &JsonValue) -> JsonValue</code> | Compiled |
| Java | <code>GET.handler(Map<String, Object> request)</code> | Compiled |
| Kotlin | <code>GET.handler(request: Map<String, Any?>)</code> | Compiled |
| C# | <code>GET.Handler(JsonElement request)</code> | Compiled |
export async function handler(request) {
return {
message: "Hello, " + request.body.name,
version: request.paths.version
}
}
// Class-based alternative
export default class GET {
async handler(request) {
return { message: "Hello from Yon" }
}
}def handler(request):
return {
"message": "Hello from Python!",
"version": request["paths"]["version"]
}
# Class-based alternative
class GET:
def handler(self, request):
return {"message": "Hello from Python"}package main
func Handler(request map[string]any) any {
return map[string]any{
"message": "Hello from Go!",
}
}pub fn handler(request: &JsonValue) -> JsonValue {
let request_id = request
.get("context")
.and_then(|ctx| ctx.get("requestId"))
.and_then(JsonValue::as_str)
.unwrap_or("unknown");
JsonValue::String(format!("request: {}", request_id))
}Compiled handlers need the language toolchain on the build machine. Tachyon generates a small JSON adapter beside the compiled binary so the handler stays dependency-free at runtime.
Create an index.html file inside any page directory to define a front-end page. Tac is Tachyon's reactive template engine — no virtual DOM, no framework. tac.bundle compiles these pages into browser modules and also prerenders them into real HTML documents for static hosting.
<!-- browser/pages/index.html -->
<script>
let count = 0
</script>
<h1>Count: {count}</h1>
<button @click="count++">Increment</button>| Syntax | Description |
|---|---|
| {expr} | Interpolate expression |
| @event="handler()" | Event binding |
| :prop="value" | Bind attribute to expression |
| :value="variable" | Two-way input binding |
| <loop :for="..."> | Loop block |
| <logic :if="..."> | Conditional block |
| <my-comp prop=val /> | Custom component resolved from components/my-comp.html |
| <ui-chart lazy /> | Lazy-loaded nested component from components/ui/chart.html |
Place .html files in browser/components/. Use them in any page with a self-closing non-native tag. Nested folders are flattened into kebab-case names automatically.
<!-- browser/components/counter.html -->
<script>
let count = 0
</script>
<button @click="count++">Clicked {count} times</button>
<!-- Use it in a page -->
<counter />
<!-- Nested component file: browser/components/ui/chart.html -->
<ui-chart />Component tags use standard custom-element style names such as <counter /> or <ui-chart />.
Add the lazy attribute to defer a component until it scrolls into the viewport. Uses IntersectionObserver internally.
<!-- Eager (default) -->
<counter />
<!-- Lazy — loaded when visible -->
<ui-chart lazy />Any package in your project's dependencies is auto-bundled and served at /modules/<name>.js. Import them dynamically in template scripts:
<script>
const { default: dayjs } = await import('/modules/dayjs.js')
let timestamp = dayjs().format('MMM D, YYYY h:mm A')
</script>
<p>Last updated: {timestamp}</p>Tachyon ships with @d31ma/fylo, a filesystem-first document store. FYLO manages collections of JSON documents with prefix indexes, event journals, WORM history, and optional AES-GCM encryption — no external database required.
Set these in your .env file:
| Variable | Description |
|---|---|
FYLO_ROOT | Directory for FYLO-managed collections (default: db/collections) |
FYLO_SCHEMA_DIR | Directory for versioned JSON schemas (default: db/schemas) |
FYLO_INDEX_BACKEND | Index backend — local-fs or s3-client (default: local-fs) |
FYLO_ENCRYPTION_KEY | Base64-encoded AES-256-GCM key for encrypted fields |
FYLO_CIPHER_SALT | Salt for key derivation |
Do not edit collections/ by hand. Document shards, indexes, and journals are managed exclusively by FYLO. Use fylo.admin rebuild <collection> to rebuild indexes from documents.
Import FYLO in your backend services and repositories to read and write documents:
import { fylo } from '@d31ma/fylo'
export class UserService {
async findByRole(role) {
const result = await fylo.users.find({
$ops: [{ role: { $eq: role } }]
})
return result.docs ?? []
}
async create(data) {
return await fylo.users.patch(crypto.randomUUID(), data)
}
async remove(id) {
return await fylo.users.del(id)
}
}The fylo global is available in Tac companion scripts without any import. Use it in reactive pages and components to query and mutate data directly from the browser:
export default class extends Tac {
/** @type {any[]} */
users = []
@onMount
async loadUsers() {
const result = await fylo.users.find()
this.users = result.docs ?? []
}
async addUser(name) {
await fylo.users.patch(crypto.randomUUID(), { name })
await this.loadUsers()
}
}Browser-side FYLO is disabled by default. Enable it with YON_DATA_BROWSER_ENABLED=true. Set YON_DATA_BROWSER_READONLY=true to allow reads only, and YON_DATA_BROWSER_REVEAL=true to decrypt encrypted fields in browser responses.
| Method | Description |
|---|---|
fylo.<collection>.find(query?) | Query documents with optional $ops filter operators |
fylo.<collection>.get(id) | Fetch a single document by ID with full history |
fylo.<collection>.list(limit?) | List documents in collection order |
fylo.<collection>.patch(id, doc) | Create or update a document |
fylo.<collection>.del(id) | Delete a document |
fylo.<collection>.events(since?) | Read the event journal from a given offset |
fylo.sql(source) | Run a SQL query against FYLO collections |
fylo.collections() | List all collections and their document counts |
fylo.setCredentials(user, pass) | Set browser-side Basic Auth credentials for FYLO requests |
fylo.clearCredentials() | Clear browser-side credentials |
Place an OPTIONS.json file in any route directory to enable validation. Set YON_VALIDATE=true in your .env.
Key response schemas by HTTP status code. Tachyon matches the handler's JSON output against each schema — the first match determines the response status code.
Enable Basic Auth by setting YON_BASIC_AUTH=user:password in your .env. Prefer YON_BASIC_AUTH_HASH in production. Comparison uses timingSafeEqual to prevent timing oracle attacks.
JWT tokens in the Authorization: Bearer header are automatically decoded and placed in context.bearer. The signature is not verified. Tachyon rejects tokens with an expired exp claim, but this check is not secure without signature verification — an attacker can forge tokens with arbitrary claims. Always verify the signature yourself (e.g., using jose) before trusting any token payload.
Create a .env file in your project root. All variables are optional.
| Variable | Description |
|---|---|
| YON_PORT | Server port (default: 8000) |
| YON_HOST | Primary bind address for the app server (default: 127.0.0.1) |
| YON_HOSTNAME | Fallback bind address for compatibility with older setups |
| YON_DEV | Enable HMR and verbose logging |
| YON_LOG_LEVEL | Log level: debug, info, warn, error (default: info) |
| YON_LOG_FORMAT | Log format: json or pretty (default: pretty) |
| YON_BASIC_AUTH | Enable Basic Auth — format: user:password |
| YON_BASIC_AUTH_HASH | Bun-hashed Basic Auth credential |
| YON_VALIDATE | Enable schema validation via OPTIONS.json files |
| YON_ALLOW_ORIGINS | CORS allowed origins |
| YON_ALLOW_METHODS | Override allowed CORS methods |
| YON_ALLOW_HEADERS | Override allowed CORS headers |
| YON_ALLOW_EXPOSE_HEADERS | Set exposed CORS headers |
| YON_ALLOW_MAX_AGE | Set CORS preflight cache duration |
| YON_ALLOW_CREDENTIALS | Enable credentialed CORS responses |
| YON_HANDLER_TIMEOUT_MS | Max handler runtime in ms (default: 30000) |
| YON_MAX_BODY_BYTES | Max request body size in bytes (default: 1048576) |
| YON_MAX_PARAM_LENGTH | Max query/path param length (default: 1000) |
| YON_CONTENT_SECURITY_POLICY | Override default CSP header |
| YON_ENABLE_HSTS | Enable HSTS header |
| YON_HSTS_VALUE | Custom HSTS header value (default: max-age=31536000; includeSubDomains) |
| YON_DEV_ERROR_DETAILS | Return handler stderr in 500 responses when true (requires YON_DEV) |
| YON_TIMEOUT | Bun server idle timeout in ms (default: 0 = no timeout) |
| YON_OPENAPI_JSON_PATH | Custom OpenAPI JSON endpoint path (default: /openapi.json) |
| YON_OPENAPI_DOCS_PATH | Custom API docs UI path (default: /api-docs) |
| YON_OPENAPI_TITLE | Override OpenAPI spec title |
| YON_OPENAPI_DESCRIPTION | Override OpenAPI spec description |
| YON_HMR_TOKEN | Token for HMR auth on non-loopback hosts (also read as YON_DEV_TOKEN) |
| YON_HMR_MAX_CLIENTS | Max concurrent HMR EventSource connections (default: 20) |
| YON_OTEL_COLLECTION | FYLO collection name for telemetry (default: otel-spans) |
| YON_ROUTES_PATH | Custom backend routes directory (default: server/routes) |
| YON_PAGES_PATH | Custom pages directory (default: browser/pages) |
| YON_COMPONENTS_PATH | Custom components directory (default: browser/components) |
| TAC_PUBLIC_ENV | Comma-separated env vars exposed to browser |
| TAC_FORMAT | Output format: esm or global (default: esm) |
Tachyon applies the following protections on every response by default:
| Area | Protection |
|---|---|
| Content-Security-Policy | Restricts resource loading |
| Strict-Transport-Security | Enforces HTTPS (when YON_ENABLE_HSTS is set) |
| X-Content-Type-Options | nosniff — prevents MIME sniffing |
| X-Frame-Options | DENY — prevents clickjacking |
| Referrer-Policy | Controls referrer header |
| Process timeout | Handlers killed after YON_HANDLER_TIMEOUT_MS |
| Body limit | Requests over YON_MAX_BODY_BYTES return HTTP 413 |
| Parameter limits | Params over YON_MAX_PARAM_LENGTH return HTTP 400 |
Tachyon ships with health, readiness, and API documentation endpoints that are always available — no configuration required.
Two pairs of endpoints for liveness and readiness probes, suitable for Kubernetes or load balancer health checks:
# Liveness — always returns 200
curl http://127.0.0.1:8080/health
# {"status":"ok","uptimeMs":12345}
curl http://127.0.0.1:8080/healthz
# {"status":"ok","uptimeMs":12345}# Readiness — 503 until the server is fully up
curl http://127.0.0.1:8080/ready
# {"status":"ready","uptimeMs":12345}
curl http://127.0.0.1:8080/readyzHealth/ready endpoints skip rate limiting and cache with no-store. /ready and /readyz return 503 until the server finishes its startup bundle — use them as Kubernetes readinessProbe targets.
Yon auto-generates an OpenAPI 3.1 spec from your route OPTIONS.json files and hosts a self-contained interactive docs UI:
The docs UI is rendered by Tachyon-owned HTML, CSS, and JavaScript — no third-party docs bundle. It supports request authorization, operation filtering, deep links, cURL generation, and live "try it out" execution with response inspection.
Customize the paths with YON_OPENAPI_JSON_PATH and YON_OPENAPI_DOCS_PATH. Override the spec title and description with YON_OPENAPI_TITLE and YON_OPENAPI_DESCRIPTION.
Yon can persist OpenTelemetry trace data into FYLO without adding an SDK dependency stack. Enable it with these env vars:
| Variable | Description |
|---|---|
YON_OTEL_ENABLED | Enable OpenTelemetry trace storage (default: false) |
YON_OTEL_ROOT | Directory for OTEL data storage |
YON_OTEL_SERVICE_NAME | Service name for span attribution |
YON_OTEL_SERVICE_VERSION | Service version for span attribution |
YON_OTEL_COLLECTION | FYLO collection name (default: otel-spans) |
YON_OTEL_CAPTURE_IP | Opt-in client IP storage (default: false) |
When enabled, Yon writes request spans and nested handler spans into the FYLO collection. Incoming traceparent headers are continued when present, and responses emit Traceparent and X-Trace-Id for correlation. Telemetry write failures are logged but do not fail the request.
Each FYLO record stores the exact OTLP JSON TracesData payload in otlpJson, plus scalar index fields such as traceId, spanId, and requestId.
export YON_OTEL_ENABLED=true
export YON_OTEL_ROOT=.tachyon-otel
export YON_OTEL_SERVICE_NAME=tachyon-dev
# Send a traced request
curl -i -H 'traceparent: 00-<trace-id>-<span-id>-01' http://127.0.0.1:8080/languages/javascriptBuild static front-end assets with tac.bundle. Output goes to dist/ with prerendered route HTML such as dist/index.html and nested route files like dist/docs/index.html.
bun run preview is the recommended frontend workflow in scaffolded apps. It serves dist/ and keeps rebuilding the bundled frontend when source files change. If you want the raw framework command, use tac.preview --watch.
bun run serve starts the full-stack app server. It detects browser/ and server/ contents and serves the frontend, backend, or both on one port.
For production deployments: