@d31ma/tachyon
v26.20.7
Published
A polyglot, file-system-routed full-stack framework for Bun
Maintainers
Readme
Features
- Polyglot backend handlers with executable files and shebangs
- Tac pages and components with
index.html/component.htmltemplates - Companion
*.js,*.ts,*.wasm, source-backed Wasm (*.as.ts,*.rs,*.c,*.go,*.zig,*.wat), and*.cssfiles beside templates - OOP-style companion classes with
export default class extends Tac - Wasm-backed Tac companions through the
tac-wasm-json@1ABI and generated adapters - Automatic persistence for
$-prefixed (sessionStorage) and$$-prefixed (localStorage) instance fields - Local-first browser
fetch()for Tac page/component scripts with IndexedDB-backed read caching and mutation-aware invalidation - Explicit browser env allowlisting through
TAC_PUBLIC_ENVandthis.env(...) - Static export with prerendered
dist/**/index.html - Shared frontend assets under
/shared/assets/* - Shared frontend data under
/shared/data/* - Generated OpenAPI 3.1 docs at
/openapi.jsonwith a self-hosted Tachyon docs UI at/api-docs - FYLO-backed OpenTelemetry storage with request and handler span correlation
- Built-in health/readiness endpoints
- Proxy-aware request context, CORS enforcement, and optional rate limiting
Install
bun add @d31ma/tachyonThat installs the public stable release from npm by default.
If you are a d31ma member and want the private beta channel from GitHub Packages instead, configure a user-level .npmrc:
# ~/.npmrc
@d31ma:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_PACKAGES_TOKEN}
always-auth=trueQuick Start
yon.init my-app
cd my-app
bun install
bun run serveUseful commands:
bun run serve is shape-aware: browser/ only bundles and serves the frontend, server/ only serves backend routes, and apps with both folders run as a full-stack app on one port.
Scaffold Layout
browser/
pages/
index.html
index.js
index.css
components/
hero/
index.html
index.css
shared/
scripts/
styles/
assets/
data/
server/
routes/
data/
deps/Scaffolds are JavaScript-first and use strict JSDoc rather than TypeScript source files. The runtime still supports TypeScript companion scripts when you want them.
The example app in examples/ demonstrates Tac and Yon working together:
- reactive page state
- persisted
$(sessionStorage) and$$(localStorage) fields - local-first fetches
- prebuilt Tac Wasm companions with source examples in WAT, AssemblyScript, Rust, C, Go, and Zig
- backend handlers in multiple languages
- shared data, shared assets, and a browser entry
- middleware, OpenAPI docs, route manifests, and component companions
Create a .env file in your app root. All variables are optional.
YON_PORT=8000
YON_HOST=127.0.0.1
YON_HOSTNAME=127.0.0.1
YON_DEV=true
YON_LOG_LEVEL=info
YON_LOG_FORMAT=pretty
YON_TRUST_PROXY=
TAC_FORMAT=esm
YON_ALLOW_HEADERS=Content-Type,Authorization
YON_ALLOW_ORIGINS=
YON_CORS_ORIGIN=
YON_ALLOW_CREDENTIALS=false
YON_ALLOW_EXPOSE_HEADERS=
YON_ALLOW_MAX_AGE=3600
YON_ALLOW_METHODS=GET,POST,PUT,DELETE,PATCH,OPTIONS
YON_BASIC_AUTH=
YON_BASIC_AUTH_HASH=
YON_VALIDATE=true
YON_CONTENT_SECURITY_POLICY=default-src 'self'; script-src 'self' 'wasm-unsafe-eval'
YON_ENABLE_HSTS=false
YON_SKIP_BUNDLE=false
YON_HANDLER_TIMEOUT_MS=30000
YON_MAX_BODY_BYTES=1048576
YON_MAX_PARAM_LENGTH=1000
YON_RATE_LIMIT_MAX=
YON_RATE_LIMIT_WINDOW_MS=
YON_ROUTES_PATH=server/routes
YON_PAGES_PATH=browser/pages
YON_COMPONENTS_PATH=browser/components
YON_ASSETS_PATH=browser/shared/assets
YON_SHARED_SCRIPTS_PATH=browser/shared/scripts
YON_SHARED_STYLES_PATH=browser/shared/styles
YON_SHARED_DATA_PATH=browser/shared/data
YON_MIDDLEWARE_PATH=./middleware
FYLO_ROOT=db
FYLO_SCHEMA_DIR=db/schemas
FYLO_INDEX_BACKEND=local-fs
YON_DATA_BROWSER_ENABLED=false
YON_DATA_BROWSER_READONLY=true
YON_DATA_BROWSER_REVEAL=falseNotes:
YON_TRUST_PROXY=loopbackis a good default when running behind a local reverse proxy.- Set both
YON_RATE_LIMIT_MAXandYON_RATE_LIMIT_WINDOW_MSto enable the built-in in-memory limiter.- For distributed deployments, export a custom
rateLimiterfrommiddleware.js.- Prefer
YON_BASIC_AUTH_HASHover plaintextYON_BASIC_AUTHin production.- FYLO-owned storage settings use the
FYLO_*prefix because they are consumed by@d31ma/fylo.- Tachyon-owned runtime settings use
YON_*orTAC_*; avoid mixed prefixes such asYON_FYLO_*.
Generate a Bun password hash with:
bun -e "console.log(await Bun.password.hash('user:pass'))"Tachyon uses @d31ma/[email protected], which is filesystem-first and uses the
FYLO local-fs index backend by default. Set FYLO_ROOT to the directory that
should contain FYLO-managed collections:
FYLO_ROOT=db
FYLO_SCHEMA_DIR=db/schemas
FYLO_INDEX_BACKEND=local-fsRun the example seed and server with the normal example environment:
cd examples
bun --env-file=.env run serveFYLO owns everything inside FYLO_ROOT, including document shards, local prefix
indexes, event journals, locks, and WORM history. With local-fs, each
collection stores compact index files under
<FYLO_ROOT>/.collections/<collection>/index/, so no external indexing service is
required.
FYLO_INDEX_BACKEND=s3-client is also passed through to FYLO when you want FYLO
to store index keys through Bun's S3 client. The old s3-prefix/LocalStack
configuration is intentionally rejected so stale deployment env cannot silently
fall back to a different backend.
Backend Routing
Yon backend routes live in server/routes.
server/
routes/ -> thin request/response controllers
languages/
javascript/GET -> GET /languages/javascript
javascript/POST -> POST /languages/javascript
javascript/PUT -> PUT /languages/javascript
typescript/GET -> GET /languages/typescript
python/GET -> GET /languages/python
ruby/GET -> GET /languages/ruby
php/GET -> GET /languages/php
go/GET -> GET /languages/go
csharp/GET -> GET /languages/csharp
java/POST -> POST /languages/java
dart/DELETE -> DELETE /languages/dart
rust/PATCH -> PATCH /languages/rust
python/versions/_version/GET -> GET /languages/python/versions/:version
typescript/items/GET -> GET /languages/typescript/items
typescript/items/POST -> POST /languages/typescript/items
typescript/items/DELETE -> DELETE /languages/typescript/items
javascript/telemetry/GET -> GET /languages/javascript/telemetry
OPTIONS.json -> route schema files
services/ -> application/business logic
repositories/ -> database and persistence access
data/ -> local example dataThe examples intentionally use an MVC-style backend dependency direction:
routes -> services -> repositories. Route files should stay small and call a
service. Services coordinate validation, business rules, and multiple
dependencies. Repositories are the only layer that talks directly to persistence
or runtime data sources. The /languages/* example is now the single backend
showcase: it includes polyglot handlers, CRUD item routes, dynamic routes, and a
telemetry consumer.
Rules:
- handler files should use their language extension, such as
GET.js,POST.ts, orPATCH.rs - the filename stem must be an uppercase HTTP method such as
GETorPOST - dynamic route segments use
_slugon disk and become:slugat runtime - the first segment cannot be dynamic
- adjacent dynamic segments are not allowed
Yon route files expose a handler(request) function. The HTTP method comes from
the filename, so server/routes/languages/typescript/items/GET.ts handles
GET /languages/typescript/items and the function name stays consistent across
every method and language.
import ItemService from '../../../../services/item-service.ts'
const service = new ItemService()
export async function handler() {
return service.listItems()
}Class-based handlers are also supported for languages that prefer that shape:
export default class GET {
async handler(request) {
return { message: 'Hello from Yon' }
}
}Handlers return plain data, FastAPI-style. OPTIONS.json decides which response
status the returned body matches. Yon serializes the returned value, validates it
when YON_VALIDATE is enabled, applies CORS/security headers, and writes the
process response internally.
Every handler receives this request object:
{
"headers": {},
"body": {},
"query": {},
"paths": {},
"context": {
"requestId": "req-id",
"ipAddress": "127.0.0.1",
"protocol": "http",
"host": "127.0.0.1:8000"
}
}If an inbound X-Request-Id header is present, Yon preserves it for upstream
correlation. Otherwise Yon generates a TTID request ID through @d31ma/fylo.
Yon invokes pure function/class handlers directly for dynamic runtimes and generates tiny build wrappers for compiled/static runtimes. No third-party adapter dependency is added; compiled handlers use the language toolchain already on the developer or deployment machine.
Java and Rust intentionally stay dependency-free. Yon generates a tiny JSON adapter beside the compiled wrapper:
- Java receives a
java.util.Map<String, Object>orObject. - Rust receives
&crate::yon_json::JsonValuefor ergonomic object access.
Rust handlers can import the generated type from the wrapper crate:
use crate::yon_json::JsonValue;
pub fn handler(request: &JsonValue) -> JsonValue {
let request_id = request
.get("context")
.and_then(|context| context.get("requestId"))
.and_then(JsonValue::as_str)
.unwrap_or("unknown");
JsonValue::String(format!("request: {}", request_id))
}OPTIONS.json files validate both incoming requests and outgoing responses with
CHEX regex schemas. Yon does not add Tachyon-specific type shorthands:
every string leaf is passed to CHEX as the regex pattern to validate.
{
"POST": {
"request": {
"body": {
"name": "^[a-z0-9-]+$",
"quantity": "^[0-9]+$"
}
},
"response": {
"201": {
"id": "^[0-9A-Za-z_-]+$",
"name": "^.{1,120}$",
"quantity": "^[0-9]+$"
},
"400": {
"detail": "^.+$"
}
}
}
}Numeric status codes may live directly under the method object, which is the style used by the example app.
Frontend Routing
Tac page routes live in browser/pages.
browser/pages/
index.html -> /
docs/
index.html -> /docs
blog/
_slug/
index.html -> /blog/:slugIf an ancestor page contains <slot />, it acts as a reusable shell for descendant pages.
Tac components live in browser/components.
browser/components/
clicker/
index.html
index.js
index.cssCompanion scripts can be JavaScript or TypeScript:
index.jsindex.ts
Tac uses one component naming convention: each component folder segment is
lowercase alphanumeric and has an index.html template. The component tag is
the folder path joined with hyphens:
browser/components/clicker/index.html -> <clicker />
browser/components/panel/users/index.html -> <panel-users />Flat templates and hyphenated folder names such as
browser/components/clicker.html and browser/components/panel-users/index.html
are rejected so app structure, generated module paths, CSS scopes, and template
tags all use the same naming rule.
Tac Templates
Templates support:
{expr}for escaped interpolation{!expr}for trusted raw HTML@event="handler()"for event binding:prop="expr"for dynamic attributes:value="field"for two-way input binding<loop :for="..."><logic :if="..."><my-component /><my-component lazy />
Example page:
<!-- browser/pages/index.html -->
<section class="hero">
<h1>{headline}</h1>
<p>{subtitle}</p>
<button @click="refresh()">Refresh</button>
</section>
<clicker label="Visits" />// browser/pages/index.js
export default class extends Tac {
/** @type {number} */
$visits = 0
/** @type {string} */
headline = 'Tac + Yon'
/** @type {string} */
subtitle = 'Reactive frontend, polyglot backend.'
constructor(props = {}, tac = undefined) {
super(props, tac)
this.$visits += 1
if (this.isBrowser) document.title = 'Home'
}
async refresh() {
const response = await this.fetch('/languages/javascript')
const payload = await response.json()
this.subtitle = String(payload.message ?? this.subtitle)
}
}Anonymous companion classes are fully supported:
export default class extends Tac {}Tac Companion Scripts
Companion scripts are instantiated automatically during render. Their fields and methods are visible in the matching HTML template without the developer manually referencing the class instance.
Companion authors only need to think about Tac itself. Internal runtime helper plumbing is attached by the framework and does not need to be imported, typed, or threaded through user code.
Available helpers through Tac:
this.env(key, fallback?)this.fetch(input, init)this.emit(name, detail)this.inject(key, fallback?)this.provide(key, value)this.onMount(fn)this.rerender()this.isBrowserthis.isServerthis.props
Wasm Companions
Tac can also load prebuilt WebAssembly companions. The browser still receives a generated JavaScript adapter that extends Tac, so templates keep the same shape:
<!-- browser/components/clicker/index.html -->
<button @click="increment()">{label}: {clicks}</button>Place the Wasm module and manifest beside the template:
browser/components/clicker/
index.html
index.wasm
index.tac.jsonOr give Tachyon source and let bun serve / bun run bundle compile it before generating the Tac adapter:
browser/components/clicker/
index.html
index.rs
index.tac.jsonSource-backed companions are selected before a sibling index.wasm, so app authors can keep a checked-in fallback while local development still compiles from source when the compiler is available. If source compilation fails and a sibling .wasm exists, Tachyon logs a warning and uses the prebuilt fallback; without a fallback, the bundle fails with the compiler error.
index.tac.json declares the stable Tac ABI:
{
"abi": "tac-wasm-json@1",
"state": {
"clicks": 0,
"label": "Ready"
},
"methods": ["increment"]
}The Wasm module must export:
memoryalloc(size) -> ptrdealloc(ptr, len)init(ptr, len)call(methodPtr, methodLen, payloadPtr, payloadLen)output_ptr() -> ptroutput_len() -> len
init receives JSON shaped as { "props": { ... } }. call receives the method name as JSON plus a payload shaped as { "args": [...], "props": { ... }, "state": { ... } }. Both functions report their response through output_ptr / output_len; the response JSON can contain { "state": { ... }, "result": ..., "effects": [...] }.
Supported effects:
{ "type": "emit", "name": "saved", "detail": { ... } }{ "type": "provide", "key": "theme", "value": "dark" }{ "type": "rerender" }
Any language can participate by compiling to a .wasm module that follows this ABI. Tachyon keeps rendering, event binding, persistence, local-first fetch, and DOM access in the generated adapter rather than exposing the DOM directly to Wasm.
Tachyon currently knows how to compile these source companions when the matching compiler is installed on the app author's machine:
Compiler path overrides are available for CI or non-standard installs:
The checked-in examples under examples/browser/components/wasm/ use real source-backed companion filenames plus sibling .wasm fallbacks, so the example app runs without requiring every language compiler to be installed:
clicker/index.watfor raw WebAssembly textassemblyscript/index.as.tsrust/index.rsc/index.cgo/index.gozig/index.zig
Tachyon intentionally treats language compilers as optional; plain .wasm works without adding a framework dependency. For strict CSP deployments, keep script-src 'wasm-unsafe-eval' in YON_CONTENT_SECURITY_POLICY so browsers can instantiate Wasm modules.
Decorator Form
The same context, lifecycle, and event helpers are also exposed as Stage 3 decorators. They move the wiring out of the constructor and onto the field or method that owns the value. Companion scripts can use them as bare identifiers — the Tachyon compiler auto-imports them when it sees the @<name> syntax, so no import line is needed in user code.
For editor and checkJs support in consuming apps, include Tachyon's ambient
globals once in the app:
/// <reference types="@d31ma/tachyon/globals" />The yon.init scaffold writes this to tachyon-env.d.ts automatically. It
lets app-authored page and component scripts use bare Tac, inject,
provide, env, onMount, emit, render, and fylo without local imports
or Cannot find name diagnostics from TypeScript-aware tooling.
If the app also uses plain ESLint no-undef, import Tachyon's globals map in
the app's flat config:
import tachyonGlobals from '@d31ma/tachyon/eslint-globals'
export default [{
files: ['browser/**/*.{js,ts}'],
languageOptions: { globals: tachyonGlobals }
}]export default class extends Tac {
/** @type {string} */
label = 'Interactions'
/** @type {string | undefined} */
@inject('demo-release', 'Tac')
release
/** @type {string} */
@provide('demo-release')
appVersion = 'TACHYON'
/** @type {number | undefined} */
@env('PUBLIC_PORT', 3000)
port
@onMount
refresh() { /* runs once after the component is attached */ }
@emit('saved')
async save(payload) { return await this.fetch('/languages/typescript/items', { method: 'POST', body: JSON.stringify(payload) }) }
}Decorator semantics:
@inject and @env mirror the underlying tac.inject / tac.env types and may return undefined when no fallback is supplied; declare the field's JSDoc type accordingly.
Outside of companion scripts (tests, library code), import the decorators explicitly:
import { inject, provide, env, onMount, emit } from '@d31ma/tachyon/decorators'Reactive Fields
Tac companion fields are reactive in the browser. Assigning to a declared instance field schedules one batched rerender automatically, so app code does not need to call this.rerender() after normal state changes:
export default class extends Tac {
count = 0
increment() {
this.count += 1
}
}$-prefixed and $$-prefixed persistent fields are reactive too, and still write through to sessionStorage / localStorage. this.rerender() remains available for rare cases where code mutates nested object/array contents in place instead of assigning a new field value.
Prop Auto-Binding
Tac automatically copies values from this.props onto any same-named instance field declared on the subclass. A leading $ or $$ on the field name is stripped when matching, so a $-prefixed or $$-prefixed persistent field automatically pairs with the unprefixed prop key:
export default class extends Tac {
/** @type {string} */
label = 'Default' // populated from props.label, falls back to 'Default'
/** @type {number} */
count = 0 // populated from props.count, falls back to 0
/** @type {number} */
$clicks = 0 // populated from props.clicks (leading $ stripped on match)
/** @type {string} */
$$theme = 'light' // populated from props.theme (leading $$ stripped on match)
}The binding runs after child class fields initialize, so the prop value wins over the field's declared default. Only fields the subclass explicitly declared participate — extra prop keys are ignored, and props and tac are skipped so a malicious-shaped props object cannot overwrite framework state. Direct matches take precedence over the stripped form, so a $clicks field paired with a props.$clicks prop uses the prefixed value rather than the unprefixed one.
Browser Environment Variables
Tac can expose explicitly public browser config through this.env(key, fallback).
export default class extends Tac {
apiBase = this.env('PUBLIC_API_BASE_URL', '/languages/javascript')
}Set the allowlist with TAC_PUBLIC_ENV:
TAC_PUBLIC_ENV=PUBLIC_API_BASE_URL,PUBLIC_SENTRY_DSN
PUBLIC_API_BASE_URL=https://api.example.comImportant boundary:
- anything sent to browser JavaScript can be seen by the browser user
- Tachyon therefore only exposes vars you explicitly allowlist
- private secrets must stay in Yon and be used through server routes, middleware, or upstream API calls made on the server
There is no secure way to give a browser script a secret and also keep that secret hidden from the browser.
API Docs
Yon exposes an OpenAPI 3.1 document at /openapi.json and a self-hosted Tachyon docs UI at /api-docs.
- route response schemas are derived from each route's
OPTIONS.jsonfile - request schemas can also flow into the OpenAPI document when you define
request - the docs page is rendered by Tachyon-owned HTML, CSS, and JavaScript instead of a third-party docs bundle
- the docs UI supports request authorization, operation filtering, deep links, cURL generation, and live "try it out" execution with response inspection
Yon can persist OpenTelemetry trace data into FYLO without adding an SDK dependency stack.
YON_OTEL_ENABLED=true
YON_OTEL_ROOT=.tachyon-otel
YON_OTEL_SERVICE_NAME=@d31ma/tachyonWhen enabled, Yon writes request spans and nested handler spans into the FYLO collection otel-spans.
- incoming
traceparentheaders are continued when present - responses emit
TraceparentandX-Trace-Idfor correlation - spans fail open: telemetry write failures are logged but do not fail the request
YON_OTEL_CAPTURE_IP=trueopt-in is required before client IPs are stored- each FYLO record stores the exact OTLP JSON
TracesDatapayload inotlpJson, plus scalar index fields such astraceId,spanId, andrequestId - custom Tachyon-specific correlation stays namespaced in span attributes such as
tachyon.request.id
Testing Telemetry
Integration coverage already exists in tests/integration/api-routes.test.js.
Run the focused check with:
bun test tests/integration/api-routes.test.jsFor a manual smoke test:
cd examples
YON_OTEL_ENABLED=true \
YON_OTEL_ROOT=.tachyon-otel \
YON_OTEL_SERVICE_NAME=tachyon-dev \
YON_BASIC_AUTH_HASH="$(bun -e "console.log(await Bun.password.hash('admin:pass'))")" \
bun ../src/cli/serve.jsThen send a traced request:
curl -i \
-H 'Authorization: Basic YWRtaW46cGFzcw==' \
-H 'X-Request-Id: manual-otel-test' \
-H 'traceparent: 00-0123456789abcdef0123456789abcdef-1111111111111111-01' \
http://127.0.0.1:8000/languages/javascriptYou should see:
TraceparentandX-Trace-Idin the response headers- persisted FYLO documents under
.tachyon-otel/.collections/otel-spans/ - one server span and one nested handler span for the request
Consuming Telemetry From FYLO
The example app includes a Yon telemetry consumer at /languages/javascript/telemetry.
- it reads
otel-spansfrom FYLO - parses the stored
otlpJsonpayload back into OTLP JSONTracesData - returns a monitoring-friendly summary plus recent spans
That route is implemented in examples/server/routes/languages/javascript/telemetry/GET.js, and the example dashboard uses it to render a live telemetry panel.
The examples also include a tiny alerting worker at examples/server/workers/telemetry-alert-worker.js.
Run it against the example app with:
cd examples
YON_TELEMETRY_URL=http://127.0.0.1:8000/languages/javascript/telemetry?limit=25 \
YON_BASIC_AUTH_HEADER='Basic YWRtaW46cGFzcw==' \
YON_ALERT_SLOW_MS=500 \
YON_ALERT_STATUS_CODE=500 \
bun run telemetry:alertsIt polls the telemetry endpoint, flags slow routes and server errors, and prints structured JSON that can be shipped to another service or cron job.
$ and $$ Field Persistence
$-prefixed instance fields are automatically persisted to sessionStorage.
$$-prefixed instance fields are automatically persisted to localStorage.
export default class extends Tac {
/** @type {number} */
$count = 0 // sessionStorage
/** @type {string} */
$$theme = 'dark' // localStorage
increment() {
this.$count += 1
}
toggle() {
this.$$theme = this.$$theme === 'dark' ? 'light' : 'dark'
}
}The persistence key is generated from:
- the Tac module path
- the current page path or generated component instance identity
- the field name (including the
$or$$prefix)
That makes the key stable across reloads and unique per persisted field instance.
Local-First fetch()
Inside Tac page/component scripts only, fetch() is wrapped with a local-first strategy:
- all request methods are supported and forwarded through the Tac wrapper
- successful
GETandHEADresponses are written to IndexedDB - later
GETandHEADcalls read from the cache first cache: 'reload'bypasses the cached read- successful non-
GETrequests invalidate cachedGETandHEADentries for the same URL so later reads do not serve stale data
This does not override the global browser fetch outside Tac page/component execution.
On the Yon side, handler responses are also given an inferred content type now:
- JSON-looking output is served as
application/json - other string output falls back to
text/plain
Scoped Component CSS
index.css is automatically wrapped with a component scope:
@scope ([data-tac-scope="clicker"]) { ... }That scope is applied to the generated wrapper around each component instance.
Shared Frontend Files
browser/shared/assets/*is served at/shared/assets/*browser/shared/data/*is served at/shared/data/*browser/shared/scripts/imports.jsis the optional browser entrybrowser/shared/styles/*is available for imports fromimports.js
If imports.js imports CSS, Tachyon emits /imports.css and links it from generated HTML shells.
The example app uses a local imports.js plus shared assets/data. Keep demo-only browser helpers out of published runtime code; shared production styles should live under browser/shared/assets or browser/shared/styles.
tac.bundle writes a static-ready dist/ directory.
yon.serve --no-bundle or YON_SKIP_BUNDLE=true starts the server without
regenerating dist/, which is useful when another build pipeline owns frontend
output. For post-processing that should run after every Tachyon bundle, export a
postBundle hook from tac.config.js:
export default {
async postBundle({ distRoot }) {
// patch distRoot/index.html, write runtime config, copy deployment assets, etc.
}
}Typical output:
dist/
index.html
docs/index.html
pages/index.js
pages/docs/index.js
components/clicker/index.js
modules/*.js
shared/assets/*
shared/data/*
shells.json
routes.json
spa-renderer.js
imports.js
imports.cssNotes:
- there is no
dist/layouts/output- page shells are represented through
shells.json- static assets are emitted under
dist/shared/assets/- the runtime now uses one app shell template and injects the HMR client only in development
Commands
Operations
Built-in endpoints:
Tachyon also supports:
- origin-aware CORS rejection before handler execution
- proxy-aware request context
- in-memory rate limiting
- middleware-provided distributed rate limiting
- cache headers for runtime assets, chunks, shared assets, and shared data
- document-request detection using browser navigation headers such as
Sec-Fetch-Dest/Sec-Fetch-Mode, withAccept: text/htmlkept as a fallback
Distributed Rate Limiting
Export a rateLimiter from middleware.js to use a shared backend.
Required env vars:
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=
YON_RATE_LIMIT_MAX=100
YON_RATE_LIMIT_WINDOW_MS=60000
UPSTASH_RATE_LIMIT_PREFIX=tachyon:rate-limitSecurity
- security headers on all responses
- Bun password verification for hashed Basic Auth
- request body and parameter limits
- handler timeout enforcement
- JWT expiry rejection when decodable
- route request/response validation through
OPTIONS.json
Production Notes
- prefer
YON_BASIC_AUTH_HASH - set explicit
YON_ALLOW_ORIGINS - configure
YON_TRUST_PROXYwhen behind nginx, Caddy, or Cloudflare - use a shared rate limiter for multi-instance deployments
- validate the built frontend with
tac.previewbefore deploy
License
MIT
