A small TypeScript service often arrives in production with much more than its application code.
It may need Node.js, a package-manager layout, production dependencies, startup scripts, environment conventions, and a container assembled to hold everything together.
That model is mature and flexible. It is also broader than every workload needs.
I have been exploring a narrower question:
What if a TypeScript service could arrive in production as one executable—and bring only the authority it had explicitly requested?
That question became Tysel, an open-source runtime for TypeScript services, workers, MCP tools, and durable agents.
The short version
During development, you still write TypeScript and can use compatible npm packages.
At build time, Tysel checks and bundles the application, combines it with a native runtime, and produces one executable:
TypeScript source
+
validated manifest
+
compatible dependencies
+
native Tysel runtime
=
one executable
The target machine does not need a separately installed Node.js runtime, V8, npm, or node_modules.
Tysel does not turn TypeScript into native machine instructions. Application JavaScript runs inside an embedded QuickJS-ng isolate. A Rust host owns HTTP, storage, secrets, resource limits, execution profiles, and durable scheduling.
The result is native as a deployment artifact, not as a new JavaScript execution model.
Try it in five minutes
Install the current stable release on Linux or macOS:
curl -fsSL https://tysel.dev/install.sh | sh
tysel doctor --install
Create and start an HTTP service:
tysel init hello-tysel --yes
cd hello-tysel
tysel task verify
tysel dev
The generated application uses the Web-standard Fetch model:
import type { TyselApp } from "@tysel/types";
export default {
async fetch(request) {
return Response.json({
message: "Hello from Tysel",
path: new URL(request.url).pathname,
});
},
} satisfies TyselApp;
Call the URL printed by the development server:
curl http://127.0.0.1:3000/hello
Then package and run it:
tysel task release
./dist/hello-tysel
The file in dist/ contains the application bundle, validated manifest, compatibility metadata, source map, and matching runtime.
A release build also emits checksums, compatibility results, an SBOM, a license inventory, and build evidence. The service runs from one executable; the additional files exist for release admission and auditing.
Under the hood
The current implementation has a deliberately small number of layers:
| Area | Current implementation |
|---|---|
| Application language | TypeScript |
| JavaScript engine | QuickJS-ng 0.16.2 |
| Native host | Rust |
| Application API | ECMAScript and a selected Web API surface |
| Delivery | One executable plus audit sidecars |
| Native targets | Linux and macOS, x64 and arm64 |
| Production isolation gate | Linux isolated workers |
| Release channels | stable and canary |
| Version status | pre-1.0 |
QuickJS is an implementation choice, not the product identity. A bare embedded engine would not provide capability manifests, deployment policy, durable history, authenticated cross-target runtimes, or release evidence.
Tysel is the contract around the engine.
Why the binary is not the main point
Node.js, Bun, and Deno already provide ways to reduce deployment complexity. Producing one file is not, by itself, a unique runtime model.
The more important Tysel decision is that host effects pass through an explicit application contract.
For example, a service that needs one API and one secret can request exactly those resources:
[permissions]
fetch = ["api.example.com"]
secrets = ["API_TOKEN"]
These declarations are requests, not unconditional authority.
Effective access is constrained by:
manifest request
∩ deployment policy
∩ execution profile
∩ runtime support
= effective capability
Adding a permission to the application manifest cannot override a stricter deployment or execution profile.
This does not make every Tysel application a hostile-code sandbox. The default service profile is designed for trusted first-party code and shares a process with the native host.
The isolated profile moves JavaScript into a separate worker process. On Linux, it adds Landlock filesystem rules, seccomp syscall filtering, resource limits, and best-effort cgroup enforcement.
Capability restriction, process isolation, container policy, and infrastructure egress controls remain separate security layers.
Durable work is part of the runtime
Agents and background workflows frequently call an external service, wait, retry, sleep, or pause for human approval.
Tysel exposes explicit durable boundaries:
const draft = await ctx.effect(
"create-draft",
() => createDraft(order),
);
await ctx.sleep("10m");
const decision =
await ctx.waitForSignal("approval");
After a restart, completed boundaries return their persisted results and execution continues against recorded history.
A completed model call or external write does not need to run again simply because the process restarted while waiting for approval.
This is not an “exactly once” claim. External systems still need idempotency keys. Durable storage remains operational state. Deployed code must preserve the meaning and ordering of boundaries used by in-flight tasks.
The goal is a visible recovery model, not pretending distributed failure has disappeared.
What the current numbers show
Tysel publishes benchmark evidence with its releases instead of presenting one number as a universal performance claim.
For the small v0.2.0 reference HTTP service:
| Published evidence | Linux x64 | Linux arm64 |
|---|---|---|
| Executable size | 19.81 MiB | 16.04 MiB |
| Cold-start p50 | 6.29 ms | 4.36 ms |
| Idle memory, PSS | 7.31 MiB | 6.72 MiB |
These measurements were collected on GitHub-hosted Ubuntu 24.04 runners. Cold start measures process launch until the service reports readiness. It is not HTTP latency or throughput.
The application is intentionally tiny, so these numbers should not be generalized to arbitrary bundles or dependencies. Their purpose is narrower: they show that the named v0.2.0 reference artifacts remain within the project’s release admission limits.
The raw evidence is public for Linux x64 and Linux arm64.
What “stable” means before 1.0
Tysel has stable and canary delivery channels:
- final SemVer releases enter
stable; - versions with a prerelease suffix enter
canary.
Stable does not mean the API is frozen.
Tysel remains pre-1.0, and APIs may change between minor releases. Stable means a final release has passed the complete compatibility, security, packaging, signing, reproducibility, and supported-platform gates.
It describes release qualification and delivery—not permanent API compatibility.
Why not use Node.js, Bun, or Deno?
You probably should if broad Node.js compatibility is the primary requirement.
Tysel intentionally does not support:
- Node.js built-ins;
- native addons;
- child processes;
- dynamic libraries;
- CommonJS loader hooks;
- arbitrary ambient operating-system access.
An npm package works only when its assumptions fit the supported ECMAScript and Web API surface.
The CLI can identify known dependency assumptions:
tysel compat --strict --deny-unknown
That report is an admission signal, not proof that every path through a dependency will work. Application tests remain necessary.
Tysel is a better candidate when the workload benefits from:
- one executable as the deployment unit;
- explicit network, secret, database, and filesystem access;
- Fetch-style HTTP APIs;
- durable sleep, retry, effects, and signals;
- a reduced execution profile;
- release evidence tied to the artifact.
This is a trade, not a free upgrade.
Current limitations
Tysel is still young, and the limitations matter:
- APIs can change between minor releases.
- npm compatibility is intentionally narrower than Node.js.
- The production isolation gate is Linux-specific.
- macOS isolation is a development check, not the production sandbox target.
- Native Windows archives are not available; Windows currently requires WSL.
- Wasm Component tasks remain experimental.
- There is no claim of an independent security audit.
- A service using the in-process profile must still be trusted first-party code.
If any of those are disqualifying, Tysel is probably not the right runtime for that application today.
The feedback I need
A runtime cannot discover its real compatibility boundary using only examples written by its author.
I need applications with unexpected dependencies, deployment environments with different assumptions, and developers willing to identify where the contract is useful—or unnecessarily restrictive.
If the idea is relevant to you, start with the small service and then try the dependency or workload you actually care about.
I would especially value answers to these questions:
- Which Node.js APIs or npm packages prevent you from trying a narrower runtime?
- Does explicit capability configuration improve deployment review?
- Is one-executable delivery operationally useful if you already deploy containers?
- Which part of the development or deployment path creates the most friction?
Top comments (0)