DEV Community

Cover image for TypeScript Without Node.js in Production: What You Gain, What You Give Up
Dave
Dave

Posted on

TypeScript Without Node.js in Production: What You Gain, What You Give Up

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Create and start an HTTP service:

tysel init hello-tysel --yes
cd hello-tysel
tysel task verify
tysel dev
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Call the URL printed by the development server:

curl http://127.0.0.1:3000/hello
Enter fullscreen mode Exit fullscreen mode

Then package and run it:

tysel task release
./dist/hello-tysel
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

These declarations are requests, not unconditional authority.

Effective access is constrained by:

manifest request
∩ deployment policy
∩ execution profile
∩ runtime support
= effective capability
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. Which Node.js APIs or npm packages prevent you from trying a narrower runtime?
  2. Does explicit capability configuration improve deployment review?
  3. Is one-executable delivery operationally useful if you already deploy containers?
  4. Which part of the development or deployment path creates the most friction?

Top comments (0)