Meet EkkoJS: The JavaScript Runtime That Doesn't Apologise for Starting Fresh

EkkoJS is a JavaScript and TypeScript runtime that trades backward compatibility for a cleaner slate. No CommonJS, no node_modules, no URL imports. This is a hands-on introduction covering the architecture, the permission model, and a working S3 demo built entirely from built-in modules.

Share
Meet EkkoJS: The JavaScript Runtime That Doesn't Apologise for Starting Fresh
EkkoJS: The JavaScript Runtime

There's a moment in every developer's day where they type npm install and watch hundreds of packages materialise from the internet. You didn't ask for most of them. A few are transitive dependencies you'll never audit. One or two probably have CVEs filed against them right now. And somehow, this became normal.

EkkoJS is a bet that it doesn't have to be.

Built by the team at Ampla Network and open-sourced under MIT, EkkoJS is a JavaScript and TypeScript runtime that trades backward compatibility for a cleaner slate. No CommonJS. No node_modules. No URL imports. Just ES modules, TypeScript that runs without a build step, security baked in at the permission level, and a standard library big enough that you won't spend the first hour of a new project hunting for packages.

It's v0.8.3 and labelled a Technology Preview as of June 2026. That means it's not ready for production systems yet. But it's already interesting enough to warrant a proper look.


What's actually running your code

The architecture is the first thing that sets EkkoJS apart. Instead of sitting on top of a single runtime engine, it layers three technologies together.

At the outermost layer is a Rust host running on the Tokio async loop. Rust handles the CLI, the process lifecycle, the permission enforcement, and the FFI bridge. It's fast and has no garbage collector, which keeps latency predictable. Inside that, V8 executes your JavaScript (and your TypeScript, after SWC transpiles it in-process). Below V8, native operations (file I/O, cryptography, HTTP) cross the FFI boundary into a .NET 10 AOT-compiled layer. Ahead-of-time compilation means that layer is native machine code, not JIT-compiled at startup.

The result: your script starts quickly, and the path from your TypeScript source to native system calls is about as short as it can get without writing Rust directly.

Here's the thing: you don't feel any of this complexity as a user. You just point ekko at a TypeScript file and it runs.


The philosophy behind it

Francois, the creator of EkkoJS, has been writing JavaScript since 2001. In his own words from the EkkoJS site: "I never felt fully comfortable with the direction JavaScript runtimes took. I kept dreaming of something different."

That frustration shapes every design decision in the runtime. Three stand out.

ESM only, local only. Every import in an EkkoJS program resolves to either a local file or a declared package. URL imports (the kind you might have seen in Deno or CDN-style scripts) are a compile-time error. This isn't just aesthetic; it closes a whole category of supply chain risk. An attacker can't slip a malicious CDN URL into your dependency graph if the runtime refuses to fetch code from the internet at import time.

Permissions as a first-class concern. Programs start with zero access to the file system, network, or environment variables. You grant what you need at the command line. Running a server that reads one database file looks like this:

ekko run --allow=net,fs:./app.db server.ts

If the server tries to read /etc/passwd or make a request to an unexpected host, it throws. The attack surface shrinks to exactly what you declared.

TypeScript without the ceremony. No tsconfig.json, no separate compile step, no output directory to manage. SWC transpiles inline, the V8 isolate picks it up, and your .ts file just runs. For small scripts and prototypes this alone saves a disproportionate amount of setup friction.


Batteries genuinely included

Over thirty modules live under the ekko: namespace. HTTP server and client. A SQL database with an ORM that supports PostgreSQL, MySQL, MSSQL, and MongoDB. Authentication with RBAC, JWT, OAuth, and TOTP. GraphQL. WebSockets. Cron and job queues. Image processing. Compression. A full-stack React framework called Rune. Even desktop and TUI app toolkits.

The HTTP server, for instance, is backed by ASP.NET Kestrel (that .NET AOT layer doing work) and comes with Express-style routing, rate limiting, CORS, and security headers built in:

import { createServer, cors, helmet, rateLimit } from "ekko:web";

const server = createServer({ port: 3000, compression: true });

server.use(cors());
server.use(helmet());
server.use(rateLimit({ max: 100, window: 60_000 }));

server.get("/users/:id", (req, res) => {
  res.json({ userId: req.params.id });
});

server.start();

No Express install. No Helmet install. No cors package. All of it ships with the runtime.


How does it perform?

Honestly, it's competitive but not dominant. And the team is transparent about that. In their own benchmark table (run on an AMD EPYC 7451, 48 threads):

  • EkkoJS finishes second overall across 35 benchmarks, behind Bun but ahead of Node.js and Deno
  • It wins the concurrency and threading category outright, with thread spawn overhead of 265/s versus Node.js at 23/s and Bun at 103/s
  • 8-way parallel scaling hits 638 ops/s against Node.js at 532 and Deno at 202
  • File read performance beats Node.js for binary reads, and Deno across the board
  • Bun leads raw single-threaded throughput, especially on maths and regex

The honest note at the bottom of the benchmarks page: "EkkoJS is a Technology Preview. We do not pretend to compete yet with Node.js, Bun, or Deno, which are production-grade, stable, and have been battle-tested for years. The comparison is a compass for our own progress."

That kind of candour is rare and worth noting.


A real example: talking to S3 via LocalStack

Here's where things get practical. Let's build a small script that uses EkkoJS to interact with an S3 bucket through LocalStack, the local AWS emulator. This demo creates a bucket, uploads a JSON document, then retrieves and logs it.

EkkoJS's ekko:crypto module provides HMAC-SHA256, which is all you need for AWS Signature Version 4. No AWS SDK. No node_modules. Just built-in modules and a handful of lines of TypeScript.

Prerequisites: Docker running with LocalStack (docker run -p 4566:4566 localstack/localstack), and EkkoJS installed. If LocalStack is new to you, I covered it in depth across a 9-part build series. Part 1 starts with S3 and presigned URLs, which maps nicely to what we're doing here.

s3-demo.ts

import { fetch } from "ekko:web";
import { hmac, hash } from "ekko:crypto";

// LocalStack defaults. Swap these for real AWS credentials and endpoint in prod.
const REGION = "us-east-1";
const ENDPOINT = "http://localhost:4566";
const ACCESS_KEY = "test";
const SECRET_KEY = "test";
const BUCKET = "ekko-demo";

// ─── AWS Signature V4 helpers ─────────────────────────────────────────────────

function toHex(bytes: Uint8Array): string {
  return Array.from(bytes)
    .map((b) => b.toString(16).padStart(2, "0"))
    .join("");
}

async function sha256Hex(data: string): Promise<string> {
  return toHex(await hash("sha256", data));
}

async function hmacSha256(key: Uint8Array | string, data: string): Promise<Uint8Array> {
  return hmac("sha256", key, data);
}

async function signedHeaders(
  method: string,
  path: string,
  service: string,
  body: string,
  extraHeaders: Record<string, string> = {}
): Promise<Record<string, string>> {
  const now = new Date();
  const date = now.toISOString().slice(0, 10).replace(/-/g, ""); // YYYYMMDD
  const datetime = now.toISOString().replace(/[:-]|\.\d+/g, "").slice(0, 15) + "Z";

  const payloadHash = await sha256Hex(body);
  const host = ENDPOINT.replace(/^https?:\/\//, "").split("/")[0];

  const headers: Record<string, string> = {
    host,
    "x-amz-date": datetime,
    "x-amz-content-sha256": payloadHash,
    ...extraHeaders,
  };

  const signedHeaderNames = Object.keys(headers).sort().join(";");
  const canonicalHeaders = Object.keys(headers)
    .sort()
    .map((k) => `${k}:${headers[k]}`)
    .join("\n") + "\n";

  const canonicalRequest = [
    method,
    path,
    "",
    canonicalHeaders,
    signedHeaderNames,
    payloadHash,
  ].join("\n");

  const credentialScope = `${date}/${REGION}/${service}/aws4_request`;
  const stringToSign = [
    "AWS4-HMAC-SHA256",
    datetime,
    credentialScope,
    await sha256Hex(canonicalRequest),
  ].join("\n");

  const signingKey = await (async () => {
    const k1 = await hmacSha256(`AWS4${SECRET_KEY}`, date);
    const k2 = await hmacSha256(k1, REGION);
    const k3 = await hmacSha256(k2, service);
    return hmacSha256(k3, "aws4_request");
  })();

  const signature = toHex(await hmacSha256(signingKey, stringToSign));

  return {
    ...headers,
    Authorization: [
      `AWS4-HMAC-SHA256 Credential=${ACCESS_KEY}/${credentialScope}`,
      `SignedHeaders=${signedHeaderNames}`,
      `Signature=${signature}`,
    ].join(", "),
  };
}

// ─── S3 operations ────────────────────────────────────────────────────────────

async function createBucket(bucket: string): Promise<void> {
  const path = `/${bucket}`;
  const headers = await signedHeaders("PUT", path, "s3", "");
  const res = await fetch(`${ENDPOINT}${path}`, { method: "PUT", headers });
  console.log(`Create bucket: ${res.status}`);
}

async function putObject(bucket: string, key: string, body: string): Promise<void> {
  const path = `/${bucket}/${key}`;
  const headers = await signedHeaders("PUT", path, "s3", body, {
    "content-type": "application/json",
  });
  const res = await fetch(`${ENDPOINT}${path}`, {
    method: "PUT",
    headers,
    body,
  });
  console.log(`Upload "${key}": ${res.status}`);
}

async function getObject(bucket: string, key: string): Promise<string> {
  const path = `/${bucket}/${key}`;
  const headers = await signedHeaders("GET", path, "s3", "");
  const res = await fetch(`${ENDPOINT}${path}`, { method: "GET", headers });
  return res.text();
}

// ─── Run ──────────────────────────────────────────────────────────────────────

await createBucket(BUCKET);

const payload = JSON.stringify({ runtime: "EkkoJS", version: "0.8.3", hello: "world" });
await putObject(BUCKET, "hello.json", payload);

const retrieved = await getObject(BUCKET, "hello.json");
console.log("Retrieved:", retrieved);

Run it with:

ekko run --allow=crypto,net s3-demo.ts

That --allow=crypto,net is EkkoJS's permission model at work: crypto gates the cryptography module, and net grants outbound network access. What's worth noting is how you discover these flags. The runtime tells you exactly what's missing. Forget --allow=crypto and you get PermissionError: crypto access denied. Run with --allow=crypto. Add it and forget --allow=net and you get PermissionError: net access denied for 'http://localhost:4566/...'. Each error names the flag and the resource it blocked. You build up the permission set incrementally, with no guessing. The script simply cannot reach anything outside what you declared.

The output:

Create bucket: 200
Upload "hello.json": 200
Retrieved: {"runtime":"EkkoJS","version":"0.8.3","hello":"world"}
EkkoJS S3 demo running against LocalStack
Ekko Terminal Commands

No packages installed. No package.json. No build output folder. Just a TypeScript file and a runtime.

CloudSprocket
S3 Bucket view in Cloud Sprocket

The wider ecosystem

EkkoJS doesn't stand alone. Three companion projects round it out:

Rune (rune.ekkojs.com) is the full-stack React framework built into the runtime. Server rendering by default, client-side navigation after the first load, and Mimir for state that survives a full browser refresh.

Bifrost (bifrost.ekkojs.com) is the package registry. Because EkkoJS refuses URL imports, it needs its own distribution channel. Bifrost is that channel. Think npm, but scoped to EkkoJS-compatible ESM packages.

Asgard (asgard.ekkojs.com) is the official component suite for Rune — 30+ accessible, themeable UI components that are server-rendered out of the box. Add it to any EkkoJS project with ekko add @ekko/asgard, wrap your app in a ThemeProvider, and you have a full component library without touching npm.


Should you use it now?

Probably not in production. The team is explicit about that: v0.8.x is a Technology Preview, and breaking changes are expected before the first production release later in 2026.

But for experimenting? For internal tooling where you control the environment? For understanding where JavaScript runtimes might go? Yes, absolutely. EkkoJS has ideas worth spending time with.

The permission model is something I wish Node.js had shipped with. The ESM-only stance closes supply chain vectors that the rest of the ecosystem still works around. The .NET AOT layer for native operations is an unusual and clever choice. And the honest benchmark page, including the numbers EkkoJS doesn't win, is a refreshing signal about the team behind it.

It's early. The echo is just starting to resonate.


EkkoJS is open source under the MIT licence. Find the runtime at ekkojs.com and the source at github.com/e-mc2-dev/ekkojs.