Tachyon

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.

Installation

Requires Bun v1.3 or later.

bash
bun add @d31ma/tachyon

Quick Start

The fastest way to start is to scaffold an app and use the generated scripts:

bash
yon.init my-app cd my-app bun install bun run preview

In a scaffolded Tachyon app, these are the recommended development commands:

bash
bun run bundle bun run preview bun run serve

Scaffold

Use yon.init to generate a deployable starter app with pages, components, entry script, package scripts, and Amplify config.

bash
yon.init my-app cd my-app bun install bun run serve

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.

Routing

Routes map directly to the filesystem. Each directory is a route segment; each file is an HTTP method.

structure
server/routes/ GET.js → GET / POST.js → POST / api/ GET.js → GET /api _version/ GET.js → GET /api/:version DELETE.js → DELETE /api/:version

Rules

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.

Handler Functions

Every route file exports a handler(request) function. The framework calls it with a request object and serializes the returned value as the response.

javascript
export async function handler(request) {
return {
message: "Hello, " + request.body.name,
version: request.paths.version,
page: request.query.page
}
}

The request object contains:

json
{ "headers": { "content-type": "application/json" }, "body": { "name": "Alice" }, "query": { "page": 1 }, "paths": { "version": "v2" }, "context": { "requestId": "req-id", "ipAddress": "127.0.0.1", "protocol": "http", "host": "127.0.0.1:8000", "bearer": { "payload": { "sub": "42", "exp": 1999999999 } } } }

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.

Polyglot Handlers

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.

Supported Languages

LanguageHandler signatureRuntime
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&lt;String, dynamic&gt; request)</code>Dynamic
Go<code>func Handler(request map[string]any) any</code>Compiled
Rust<code>pub fn handler(request: &amp;JsonValue) -&gt; JsonValue</code>Compiled
Java<code>GET.handler(Map&lt;String, Object&gt; request)</code>Compiled
Kotlin<code>GET.handler(request: Map&lt;String, Any?&gt;)</code>Compiled
C#<code>GET.Handler(JsonElement request)</code>Compiled

Examples

GET.js — JavaScript
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" }
}
}
GET.py — Python
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"}
GET.go — Go (compiled)
package main
func Handler(request map[string]any) any {
return map[string]any{
"message": "Hello from Go!",
}
}
GET.rs — Rust (compiled)
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.

Front-end (Tac)

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.

html
<!-- browser/pages/index.html -->
<script>
let count = 0
</script>

<h1>Count: {count}</h1>
<button @click="count++">Increment</button>

Template Syntax

SyntaxDescription
&#123;expr&#125;Interpolate expression
@event="handler()"Event binding
&#58;prop="value"Bind attribute to expression
&#58;value="variable"Two-way input binding
&lt;loop &#58;for="..."&gt;Loop block
&lt;logic &#58;if="..."&gt;Conditional block
&lt;my-comp prop=val /&gt;Custom component resolved from components/my-comp.html
&lt;ui-chart lazy /&gt;Lazy-loaded nested component from components/ui/chart.html

Components

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.

html
<!-- 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 />.

Lazy Loading

Add the lazy attribute to defer a component until it scrolls into the viewport. Uses IntersectionObserver internally.

html
<!-- Eager (default) -->
<counter />

<!-- Lazy — loaded when visible -->
<ui-chart lazy />

NPM Modules

Any package in your project's dependencies is auto-bundled and served at /modules/<name>.js. Import them dynamically in template scripts:

html
<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>

FYLO Storage

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.

Configuration

Set these in your .env file:

VariableDescription
FYLO_ROOTDirectory for FYLO-managed collections (default: db/collections)
FYLO_SCHEMA_DIRDirectory for versioned JSON schemas (default: db/schemas)
FYLO_INDEX_BACKENDIndex backend — local-fs or s3-client (default: local-fs)
FYLO_ENCRYPTION_KEYBase64-encoded AES-256-GCM key for encrypted fields
FYLO_CIPHER_SALTSalt for key derivation

Directory Structure

structure
db/ ├─ schemas/ # Versioned JSON schemas for strict validation │ └─ users/ │ ├─ manifest.json │ ├─ history/ │ │ └─ v1.json │ └─ rules.json # Optional RLS rules └─ collections/ # Document store (managed by FYLO) └─ users/ ├─ .fylo/ # Prefix indexes, journals, locks, WORM history └─ *.json # Document shards

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.

Server-Side Usage

Import FYLO in your backend services and repositories to read and write documents:

javascript — services/user-service.js
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)
}
}

Browser-Side Usage

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:

javascript — component companion
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.

FYLO API Reference

MethodDescription
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

Schema Validation

Place an OPTIONS.json file in any route directory to enable validation. Set YON_VALIDATE=true in your .env.

json
{ "POST": { "request": { "body": { "name": "^[a-z0-9-]+$", "quantity": "^[0-9]+$" } }, "response": { "201": { "id": "^[0-9A-Za-z_-]+$", "name": "^.1$", "quantity": "^[0-9]+$" } } } }

Status Codes

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.

json
{ "POST": { "request": { "body": { "name": "^.+$" } }, "201": { "id": "^[0-9A-Za-z_-]+$", "name": "^.+$" }, "400": { "detail": "^.+$" }, "503": { "detail": "^.+$", "retryAfter": "^[0-9]+$" } }, "DELETE": { "204": {} } }

Auth

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.

Configuration

Create a .env file in your project root. All variables are optional.

VariableDescription
YON_PORTServer port (default: 8000)
YON_HOSTPrimary bind address for the app server (default: 127.0.0.1)
YON_HOSTNAMEFallback bind address for compatibility with older setups
YON_DEVEnable HMR and verbose logging
YON_LOG_LEVELLog level: debug, info, warn, error (default: info)
YON_LOG_FORMATLog format: json or pretty (default: pretty)
YON_BASIC_AUTHEnable Basic Auth — format: user:password
YON_BASIC_AUTH_HASHBun-hashed Basic Auth credential
YON_VALIDATEEnable schema validation via OPTIONS.json files
YON_ALLOW_ORIGINSCORS allowed origins
YON_ALLOW_METHODSOverride allowed CORS methods
YON_ALLOW_HEADERSOverride allowed CORS headers
YON_ALLOW_EXPOSE_HEADERSSet exposed CORS headers
YON_ALLOW_MAX_AGESet CORS preflight cache duration
YON_ALLOW_CREDENTIALSEnable credentialed CORS responses
YON_HANDLER_TIMEOUT_MSMax handler runtime in ms (default: 30000)
YON_MAX_BODY_BYTESMax request body size in bytes (default: 1048576)
YON_MAX_PARAM_LENGTHMax query/path param length (default: 1000)
YON_CONTENT_SECURITY_POLICYOverride default CSP header
YON_ENABLE_HSTSEnable HSTS header
YON_HSTS_VALUECustom HSTS header value (default: max-age=31536000; includeSubDomains)
YON_DEV_ERROR_DETAILSReturn handler stderr in 500 responses when true (requires YON_DEV)
YON_TIMEOUTBun server idle timeout in ms (default: 0 = no timeout)
YON_OPENAPI_JSON_PATHCustom OpenAPI JSON endpoint path (default: /openapi.json)
YON_OPENAPI_DOCS_PATHCustom API docs UI path (default: /api-docs)
YON_OPENAPI_TITLEOverride OpenAPI spec title
YON_OPENAPI_DESCRIPTIONOverride OpenAPI spec description
YON_HMR_TOKENToken for HMR auth on non-loopback hosts (also read as YON_DEV_TOKEN)
YON_HMR_MAX_CLIENTSMax concurrent HMR EventSource connections (default: 20)
YON_OTEL_COLLECTIONFYLO collection name for telemetry (default: otel-spans)
YON_ROUTES_PATHCustom backend routes directory (default: server/routes)
YON_PAGES_PATHCustom pages directory (default: browser/pages)
YON_COMPONENTS_PATHCustom components directory (default: browser/components)
TAC_PUBLIC_ENVComma-separated env vars exposed to browser
TAC_FORMATOutput format: esm or global (default: esm)

Security

Tachyon applies the following protections on every response by default:

AreaProtection
Content-Security-PolicyRestricts resource loading
Strict-Transport-SecurityEnforces HTTPS (when YON_ENABLE_HSTS is set)
X-Content-Type-Optionsnosniff — prevents MIME sniffing
X-Frame-OptionsDENY — prevents clickjacking
Referrer-PolicyControls referrer header
Process timeoutHandlers killed after YON_HANDLER_TIMEOUT_MS
Body limitRequests over YON_MAX_BODY_BYTES return HTTP 413
Parameter limitsParams over YON_MAX_PARAM_LENGTH return HTTP 400

Built-in Ops Endpoints

Tachyon ships with health, readiness, and API documentation endpoints that are always available — no configuration required.

Health & Readiness

Two pairs of endpoints for liveness and readiness probes, suitable for Kubernetes or load balancer health checks:

bash
# 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/readyz

Health/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.

API Documentation

Yon auto-generates an OpenAPI 3.1 spec from your route OPTIONS.json files and hosts a self-contained interactive docs UI:

structure
GET /openapi.json → Live OpenAPI 3.1 spec GET /api-docs → Interactive API 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.

OpenTelemetry

Yon can persist OpenTelemetry trace data into FYLO without adding an SDK dependency stack. Enable it with these env vars:

VariableDescription
YON_OTEL_ENABLEDEnable OpenTelemetry trace storage (default: false)
YON_OTEL_ROOTDirectory for OTEL data storage
YON_OTEL_SERVICE_NAMEService name for span attribution
YON_OTEL_SERVICE_VERSIONService version for span attribution
YON_OTEL_COLLECTIONFYLO collection name (default: otel-spans)
YON_OTEL_CAPTURE_IPOpt-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.

bash — manual smoke test
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/javascript

Production

Build 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.

bash
bun run bundle bun run preview # app server only bun run serve # raw framework commands tac.preview --watch tac.bundle --watch

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:

  • Set YON_BASIC_AUTH to a strong credential — never commit real values
  • Set YON_ALLOW_ORIGINS to your application's domain instead of *
  • Deploy dist/ directly to static platforms such as Amplify after running tac.bundle
  • Add a reverse proxy (nginx, Caddy) for HTTPS and rate limiting when serving the Bun app directly