DEV Community

Cover image for EkkoJS, the new challenger
Hello
Hello

Posted on

EkkoJS, the new challenger

Somewhere between the third config file and the second build step, I stopped having fun.

It crept up on me slowly. The work I actually wanted to do, writing code that solves a problem, kept getting pushed further and further behind the work of getting ready to write that code. Scaffolding, configuring, wiring tools together, then maintaining all of it. I have been writing JavaScript since 1999, so I had collected a lot of opinions about what was wrong and very little to show for them.

So I built the runtime I always wanted to use. Three years ago I took the decision to start, in 2024 I sealed the name and took the domain, and now, in the middle of 2026, EkkoJS is finally here, at ekkojs.com.

A fresh start

If I am going to build a new runtime, then I am going to start fresh. No dragging the past along just to make a few people feel at home on day one. Every decision below is one I made on purpose, knowing it would cost me some compatibility, because the whole point was to stop paying for the old mistakes.

Help command

Bye bye CommonJS

The future is modular, so let's cut the bridge with the past. EkkoJS is, and stays, ESM only. No compromise.

Yes, that alone walks away from the CommonJS world, and from the easy interop everyone else keeps alive. I am fine with that. The two module systems were never meant to live together. CommonJS loads synchronously, ESM loads asynchronously, and that single difference is why every runtime that supports both ends up with a pile of special cases to paper over the seam.

I did not want that pile. One way in, one way out. A static import, top to bottom.

import { readText } from "ekko:fs";        // built-in module, ekko: prefix
import { connect } from "ekko:db/orm";      // built-in, sub-path
import { Button } from "@ekko/asgard";      // a package from the store
import { formatUser } from "./lib/user";    // local file, extensionless

// No require(), no module.exports, no __dirname. There is nothing else to learn.
export function greet(name: string) {
  return `Hello, ${name}`;
}
Enter fullscreen mode Exit fullscreen mode

Imports are always extensionless. Whether the file is .js, .ts, .jsx, .tsx or .mjs, you import it by name and the runtime resolves the rest. The runtime enforces it at build time, so writing the extension is an error. One rule, no exceptions, nothing to argue about in a review.

And when you genuinely need to load something at runtime, dynamic import() is there too, same rules, same resolution.

// load it only when you actually need it
const { runMigration } = await import("./db/migrate");
Enter fullscreen mode Exit fullscreen mode

Bye bye node_modules

Here is the one most people will trip over, so let me be direct. EkkoJS does not have a node_modules. Dependencies do not get copied into your project folder, ever.

When you add a package, it goes into a single local store on your machine, one copy per version, shared by every project that asks for it.

ekko add @ekko/asgard          # resolves, downloads, installs into the store
ekko add @ekko/asgard@1.0.0    # a specific version
ekko add --build some-tool     # a build-only dependency
Enter fullscreen mode Exit fullscreen mode

Your project keeps a short list of what it depends on in ekko.json, and the exact versions are pinned in ekko.lock. That is it. No folder with thousands of nested directories, no copy of the same library sitting in fifteen different projects, no waiting for the disk to finish writing a small country's worth of files before you can start.

The mental model is simple. Your project declares what it needs. The store holds the bytes. The same version is installed once and reused everywhere, instead of being copied so many times the folder grows its own gravity.

TypeScript is a first class citizen

TypeScript is a first class citizen here, treated exactly like JavaScript, with no configuration needed.

There is no tsconfig.json to write, no compiler to install, no build step to wire up before you can run a single line. You point EkkoJS at a .ts file and it runs.

ekko run main.ts
ekko run main        # the extension is optional, same as everywhere else
Enter fullscreen mode Exit fullscreen mode

You can mix .ts and .js in the same project without thinking about it, because imports are extensionless and the runtime resolves the right file for you. Types are there while you write, and they get out of the way when you run.

We never had to configure JavaScript to make it run. With EkkoJS, you do not configure TypeScript either. It just runs.

More than a runtime

A runtime that only runs scripts gets dull fast. What I wanted was a single tool that could build the things I actually build, without dragging in a different stack for each one.

So EkkoJS ships the targets out of the box, all from the same runtime, the same language, and the same store:

  • Libraries. Write a package, pack it into a single file, share it. The thing you build is the thing other projects depend on.
  • Web, full stack. React on the front, a real backend behind it, SEO handled. This is Rune, and it gets its own section below.
  • CLI. Real command line tools, with argument parsing, options and colored output, in a handful of lines.
  • TUI. Rich terminal applications with panels, layouts and live updates.
  • Desktop. Native desktop applications, with no Electron and no rebuilding all your native dependencies just to ship a window.

All of this comes from the runtime itself, not from a pile of third party packages glued together and prayed over. One install, and you can reach for any of these the moment you need it.

One file to ship: the .ekl

When a project is ready to leave your machine, you pack it.

ekko pack
# → my-project-1.0.0.any.any.ekl
Enter fullscreen mode Exit fullscreen mode

That single .ekl file is the whole project: your transpiled sources, the native libraries it needs, the assets it reads, and the ekko.json and ekko.lock so it resolves its dependencies exactly the way it did on your disk. Everything is compressed per entry, checksummed with SHA-256, and optionally signed with ECDSA. One file to ship, one file to run, and a dist folder you never have to babysit.

A runnable project starts straight from the archive, with no extraction step:

ekko run --ekl my-app.ekl --allow=all
Enter fullscreen mode Exit fullscreen mode

An .ekl is more than something you publish to a registry. Anything you can run in EkkoJS runs from one just as well, a CLI tool, a server, a full Rune site, all from that single file.

The .ekl is the filesystem

Eko inspect

Which leads to the part I find elegant. When an app runs from its .ekl, the archive becomes its filesystem.

Every ekko:fs read resolves against an in-memory virtual filesystem built from everything you packed. Reads look in that VFS first and only fall back to the real disk if the path was not packed. So the app always finds its own bundled assets, wherever you copied the file, and reading its own resources needs no fs permission, because it is reading itself, not the host's disk.

Writes are the mirror image. They always go to the real filesystem, rooted next to the archive, so the app can read its sealed contents and still write where it is allowed to. The bundle stays read-only and honest, and the host stays in control of what gets touched.

A rich standard library

Like any runtime worth using, EkkoJS ships a deep standard library, so you can start building immediately instead of shopping for a dependency every time you need to hash a string or open a socket. Every module lives behind the ekko: prefix, and every one of them answers to the permission system.

Here is the surface, grouped by what you would reach for.

System and I/O

Module What it gives you
ekko:fs File system access, synchronous and CWD-relative
ekko:fs/path Path building and normalization, cross-platform
ekko:net Raw TCP and UDP sockets and DNS, client and server
ekko:process Spawn child processes, read env, args, signals, exit codes
ekko:ffi Call into native C-ABI libraries directly from JS or TS

Data, text and time

Module What it gives you
ekko:crypto Hashing, AES and RSA encryption, key derivation, ECDSA signatures, secure random
ekko:compress Compression and decompression, gzip, deflate and brotli
ekko:datetime Dates, durations and timezones with real IANA support
ekko:image Image decoding, encoding, resizing and transformation
ekko:text/json JSON parsing and serialization beyond the built-in JSON
ekko:text/encoding Base64, hex and UTF-8 encoders and decoders
ekko:text/regex Regular-expression helpers

Databases

Module What it gives you
ekko:db Low-level access across SQLite, Postgres, MySQL, SQL Server and Mongo
ekko:db/orm A typed query builder over the same drivers, swap the driver without rewriting your queries

Web and real time

Module What it gives you
ekko:web HTTP and WebSocket server with security middleware built in
ekko:web/validate Schema validation for bodies, params and queries
ekko:web/graphql A GraphQL server that plugs into ekko:web
ekko:web/realtime Real-time channels and rooms over WebSocket

Auth and access control

Module What it gives you
ekko:auth Sessions, JWT and password hashing
ekko:auth/rbac Role-based access control, roles, permissions and policy checks

Full stack (Rune)

Module What it gives you
ekko:rune The full-stack framework, createApp, pages and middleware
ekko:rune/router SSR-first then client-side routing, Link and useRouter
ekko:rune/mimir Mimir, the cross-frontier state management
ekko:rune/seo SEO helpers, canonical URLs, sitemap and robots
ekko:ssr The standalone server-side rendering engine
ekko:ssr/css Scoped Sass and CSS modules for server rendering

Application targets

Module What it gives you
ekko:app/cli CLI framework, command parser, colored output and prompts
ekko:app/tui TUI framework for rich terminal applications
ekko:app/gui GUI framework for native desktop applications

Jobs, logging and testing

Module What it gives you
ekko:job/cron Cron-style scheduled jobs
ekko:job/queue Background job queues with retries
ekko:log Structured logging with levels and formats
ekko:test The built-in test runner, describe, test, expect, mocks and coverage
ekko:test/assert A standalone assertion library

On top of all of these sits the global Ekko namespace for the runtime itself, with Ekko.spawn, Ekko.parallel and Ekko.Channel for threading, plus Ekko.args and Ekko.env.

Native code, the easy way

Calling into a native library should be boring, so in EkkoJS it is. You point ekko:ffi at a shared library, declare the functions you want with their argument and return types, and call them like any other function.

import { dlopen, types } from "ekko:ffi";

// The C math library has a different name on each OS, so pick it from Ekko.platform.
const MATH = { win32: "msvcrt.dll", darwin: "libSystem.B.dylib" }[Ekko.platform] ?? "libm.so.6";

const libm = dlopen(MATH, {
  sqrt: { args: [types.f64], returns: types.f64 },
  pow:  { args: [types.f64, types.f64], returns: types.f64 },
});

console.log(libm.sqrt(144));   // 12
console.log(libm.pow(2, 10));  // 1024
libm.close();
Enter fullscreen mode Exit fullscreen mode

That is the whole ceremony. The runtime handles the conversions across the boundary, numbers, strings, buffers and pointers, and FFI still answers to the permission system, so a package can call its own bundled binary while anything external stays behind a deliberate, named grant.

And here is where it pays off on the desktop. Anyone who has built a desktop app on the usual stack knows the native rebuild dance, where the moment Node and Electron disagree on an ABI you are recompiling native modules and praying. EkkoJS ships its own native layer and packs the right binary straight into your .ekl, so the desktop app carries the native code it needs and runs. No rebuild step, no version roulette between two runtimes that were never quite in sync.

Testing is part of the deal

If I want everything I need out of the box, then testing has to be in the box too. Fully. So EkkoJS ships a complete test framework and a coverage reporter inside the runtime, with nothing to install and nothing to configure.

The API is the one you already know. describe, test, expect, plus mocks when you need them.

// math.test.ts
import { describe, test, expect } from "ekko:test";

describe("addition", () => {
  test("adds two numbers", () => {
    expect(1 + 1).toBe(2);
  });

  // async tests are awaited for you
  test("resolves a value", async () => {
    expect(await Promise.resolve(42)).toBe(42);
  });
});
Enter fullscreen mode Exit fullscreen mode

Name your files *.test.ts and run them. The runner discovers them on its own, needs no permissions, and exits non-zero when something fails, so it drops straight into CI without ceremony.

ekko test                  # discover and run every *.test.ts
ekko test math.test.ts     # run a single file
ekko test --coverage       # collect a coverage report while you are at it
Enter fullscreen mode Exit fullscreen mode

Coverage is built in the same way. One flag produces an lcov report and an HTML view you can open in a browser, for both JavaScript and TypeScript, with no extra tooling to bolt on.

Security first, deny by default

A program in EkkoJS starts with nothing. No file system, no network, no environment, no process spawning, no crypto, no FFI. The first time the code reaches for a native capability it was not granted, it gets a PermissionError and stops.

You decide what it can touch, with one flag.

ekko run app.ts                          # no grants, native calls throw
ekko run --allow=fs,net,crypto app.ts    # grant whole categories
ekko run --allow=all app.ts              # grant everything, for when you are just hacking
Enter fullscreen mode Exit fullscreen mode

While you are building, --allow=all keeps you out of your own way. When you ship, you grant exactly what the code needs, and you can scope each grant down to a path, a host or a single variable.

ekko run --allow=fs:./data/** app.ts            # the file system, but only under ./data
ekko run --allow=net:api.example.com app.ts     # the network, but only to one host
ekko run --allow=env:DATABASE_URL app.ts        # one environment variable, nothing else
Enter fullscreen mode Exit fullscreen mode

The path checks resolve symlinks before they decide, so a link inside an allowed folder that points somewhere it should not goes nowhere. A ./data/link aimed at /etc/passwd is denied even with fs:./data/**.

You can also write the grants once in ekko.json so you never retype a flag, and the CLI still wins when you need to override:

{
  "permissions": {
    "fs": true,
    "net": true,
    "env": ["DATABASE_URL", "API_KEY"]
  }
}
Enter fullscreen mode Exit fullscreen mode

The doctrine behind it is simple. Developer tools like ekko test and ekko dev run with full trust, because that is your machine and your code. Shipped code, run with ekko run, gets the smallest set of capabilities that lets it do its job. The runtime makes the safe path the easy one, so locking things down is the default rather than the heroic afterthought.

Rune, the full stack framework

Rune

We have all lived this one. You build a slick client-side app, everything is fast and smooth, and then someone asks the obvious question: how does Google see it? So you bolt on server-side rendering, and suddenly every navigation is a round trip to the server, and the snappy app you were proud of feels like it is wading through mud. You traded performance for SEO, and nobody is happy.

Or the other classic. The app is gorgeous and client-side first, and then a user presses F5 halfway through a flow and the whole thing forgets who it is. Empty screen, lost context, back to the start. One tap of the refresh key and your app has amnesia.

Rune is my answer to both. You can see it at rune.ekkojs.com.

The philosophy fits in one line: server-rendered on the first load, fully client side after. The first paint comes from the server, so any URL you land on is real and indexable, which keeps SEO happy. From there you are a single-page app, navigation stays on the client, and nothing does a useless round trip just to redraw a page. You get the SEO without paying for it in performance.

A page is a React component, plus an optional ssr() that runs on the server for that first load and feeds the metadata.

// pages/index.tsx
// ssr() runs on the server, on first load only, and sets the page metadata,
// so the URL is fully indexable before any JavaScript runs.
export function ssr() {
  return { title: "Home, EkkoJS", description: "Server-rendered, then client side." };
}

// After that first paint, this is a normal React app, navigation stays on the client.
export default function Home() {
  return <h1>Hello from Rune</h1>;
}
Enter fullscreen mode Exit fullscreen mode

And the refresh problem? Rune ships Mimir, a state library that lives across the frontier. The same state flows out of the server render, into the client, and keeps going there, surviving a refresh. So F5 stops being the button that wipes your app's memory.

That is the whole idea. SEO without sacrificing performance, and a client-side app that does not panic the moment someone reaches for refresh.

Asgard, the component suite

Asgard

A framework is more pleasant when you are not drawing every button from scratch, so EkkoJS has an official component suite, Asgard.

It is a batteries-included React component library made for Rune apps. Buttons, text fields, selects and switches, date and color pickers, data tables, tree views, alerts, toasts, tooltips and drawers, a real layout engine, and an IDE-grade docking system for the kind of app that has panels you drag and split. The components render on the server with Rune and hydrate cleanly on the client, so they fit the SSR-first model without a fight.

Time Picker

You add it like any other package and wrap your app in a theme.

import { ThemeProvider, Button } from "@ekko/asgard";
import { themes } from "@ekko/asgard/theme";

export default function App() {
  return (
    <ThemeProvider theme={themes.nord}>
      <Button variant="filled">Get started</Button>
    </ThemeProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

Theming runs deep. There are 27 built-in palettes, Nord, Dracula, Tokyo Night, Catppuccin and more, or you bring your own, and a bridge that re-themes your own SCSS the moment you switch. Keyboard navigation, focus management and ARIA roles come built in, so accessibility is the starting point rather than a later cleanup.

Bifrost, the registry

Bifrost

A package format is only half the story. The other half is somewhere to send it, so EkkoJS has its own registry, Bifrost.

The flow is the one you would expect. You pack your project into a single .ekl, you log in once, and you publish.

ekko login                  # stores a token in ~/.ekko/config.json
ekko publish --dry-run      # see exactly what would be uploaded, no surprises
ekko publish                # pack and push to Bifrost
Enter fullscreen mode Exit fullscreen mode

On the other side, installing it is the same one command as any other dependency, and it lands in the shared store rather than in a folder inside the project.

ekko add @me/app
Enter fullscreen mode Exit fullscreen mode

Every package is versioned, and if you have a signing key it is signed with ECDSA on the way up, so what you publish is what people install, provably. The same crypto runs in the CLI and in the registry, one implementation on both ends, so there is no seam where a signature means one thing here and another thing there.

Registry

Visibility is yours to set in ekko.json, public or private, and the registry checks quota and validity before a single byte is uploaded. You always know what is going out before it goes.

Zero-shot runtime coding

I can already hear the objection. "A brand new runtime in 2026? My AI assistant has never heard of it. I will be flying blind, autocompleting into the void, fighting a model that keeps trying to write me a package.json."

It is a fair fear. It is also the first thing I solved.

The entire documentation ships inside the binary. ekko doc browses it right in your terminal, across three topics: the runtime, the Asgard components, and the Rune framework. You list a topic's pages, then open any one of them.

ekko doc js                 # list the runtime's pages, each one numbered
ekko doc js 007             # open a page (here, Permissions)
ekko doc js 007 --llm       # the same page as raw markdown, written for a machine
Enter fullscreen mode Exit fullscreen mode

That last flag is the one that matters for tooling. --llm prints the page as clean markdown, so an AI assistant can list the pages and pull whichever ones it needs, on demand, as plain text.

Drop the flag and you get the same page rendered for a human, formatted right there in the terminal, with headings, tables and syntax-highlighted code. And if you would rather read and explore than type page numbers, -i opens a full interactive browser, a split-pane TUI you navigate with the keyboard.

ekko doc js 007             # human-friendly, rendered in the terminal
ekko doc js -i              # full interactive TUI, browse the whole topic
Enter fullscreen mode Exit fullscreen mode

No fine-tuning, no prior training, no waiting for the next model to maybe learn about EkkoJS. Any capable AI can read the live, exact documentation for the version you are actually running, and code against it from a standing start. Point your assistant at ekko doc and it learns the runtime as well as the docs do, today, not whenever a training cut-off catches up.

So the runtime being new stops being a problem and starts being the point. It documents itself, for you and for the machine sitting next to you, and the guidance is always current because it comes from the same binary that runs your code.

Where it stands

Let me be honest about the stage we are at. EkkoJS is a technology preview until October. Breaking changes are already on the roadmap, on purpose, because I would rather get the design right in the open than freeze the wrong decisions early.

So here is my ask. Test it. Build something real with it. Push on it and tell me where it hurts. But please, do not ship it to production yet. That day is coming, and this is not it.

Stay tuned. A lot of articles and tutorials are on the way, and I will be walking through every part of this in more depth. This was the overview. The rest is coming.

Try it

Installing is a single command, and that command is the whole toolchain. It runs your code, manages your packages, runs your tests, and builds your projects.

# macOS / Linux
curl -fsSL https://ekkojs.com/install.sh | sh
Enter fullscreen mode Exit fullscreen mode
# Windows (PowerShell)
irm https://ekkojs.com/install.ps1 | iex
Enter fullscreen mode Exit fullscreen mode

Then write your first line.

// main.ts
console.log("Hello, EkkoJS!");
Enter fullscreen mode Exit fullscreen mode
ekko run main.ts
Enter fullscreen mode Exit fullscreen mode

That is it. No tsconfig, no bundler, no folder full of dependencies. If you have read this far, the best thing you can do is install it, build something small, and tell me what you think. EkkoJS is born from the Norwegian word for echo, so write once, and let it resonate.

Enjoy using it.

Francois, creator of EkkoJS.

Top comments (0)