Today we are sharing our first iteration of the Playbit runtime, our vision for building playful personal-scale software.

Personal-scale software means programs by you, for you and for the people in your life. An app for your friends isn't very useful if only some of them can run it, so usually these projects have only one option: the web, an abstraction which many apps don't fit well into.

We wanted a better solution, so that's what we're building. A runtime designed for highly dynamic graphical apps that are collaborative, with a really good set of developer tools.

The Playbit runtime is a bit like an OS, but lives inside a host environment and gives guest code a small system layer to interface with. In practice it’s a minimal ABI-stable syscall interface with well-defined semantics.

While we only support macOS in this initial release, our vision is for a powerful multimedia and collaborative platform which you can write your app for once, and run it on any platform, be it desktop, web, or mobile. Run your apps as WASM on the runtime or natively by linking the Playbit runtime as a native-code library.

With love and a bit of code,

Edward, Nick, Rasmus, and Julia
The Playbit Team

Playbit for macOS Requires Apple Silicon

Things to try

Example

#include <playbit/playbit.h>

f32 x = 0, y = 0;

void on_event(const PBEvent* eventHeader) {
    const PBEventAny* event = (const PBEventAny*)eventHeader;
    if (event->type == PBEventType_POINTER_MOVE) {
        x = event->pointer.x;
        y = event->pointer.y;
    }
}

void update_ui() {
    PBDrawRect(PBRectangleMake(x, y, x + 132, y + 132), PBColor_Red);
    PBDrawText(
        PBUIFontMake(PB_STR("Inter"), 16, 500),
        PB_STR("Hello world!"),
        PBColor_White,
        PBRectangleMake(x + 16, y + 16, x + 100, y + 100));
}

void timer_rang(PBTimer timer, u64 arg) {
    log("timer rang");
}

void main() {
    PBAppTimeout(100 * PB_TIME_MILLISECOND, timer_rang, 0);
    PBApp.onEvent = on_event;
    PBApp.onRender = update_ui;
    PBAppMain();
}

Why

Writing cross-platform software is harder than it should be. The web stack can be a poor fit for dynamic apps & games, and native platforms are often cumbersome and fragile to work with. Playbit is our attempt to make app development simpler and more enjoyable again.

How it works

You build against a small platform layer, then get higher-level tools on top when you need them. Our goal is for the same app to eventually run in the browser, in the Playbit app, or as a custom native app. Start with one target. Grow into more control later. We give you a solid base of features so you can spend more time making the thing you actually want to build.

What it is for

Playbit is for software that needs to be responsive, expressive, and close to the system. Think creative tools, editors, multiplayer apps, utilities, and other software that does not sit comfortably inside a browser-shaped box.

Natively Cross-platform

Our vision is that one app should be able to reach macOS, Windows, Linux, iOS, Android, and the web without making you rebuild the whole thing six times. Playbit tries to keep that path practical, while still leaving room for native packaging and platform-specific escape hatches.

Start simple, go deeper

It should be easy to get something running fast. Send a link, test an idea, share it with a friend. When the project grows, you will eventually be able to dig deeper into custom UI, native integrations, debugging tools, and packaging without throwing away the foundation.

We want the default path to feel calm and obvious. Fewer choices, better ones. Strong building blocks. Clear escape hatches. The point is not to hide complexity forever; to let complexity arrive only when it is useful.

More than a runtime

Over time, Playbit will support the workflow around the app: debugging, profiling, syncing, distribution, and collaboration. The runtime comes first, but the goal is a platform that helps with the full life of making software.

playbit runtime

The Playbit runtime feels a bit like an OS kernel, but lives inside a host environment and gives guest code a small system layer to interface with. In practice it's a minimal ABI-stable syscall interface with well-defined semantics.

Capabilities first

Everything important is an object behind a handle: threads, channels, streams, clocks, windows, gpu, text plans. Access rights live on the handle, not in ambient process state. If code does not hold the handle, or the handle lacks the right, that action is simply not available. For example, you may be able to write to a stream but not read from it.

Sandboxes that can nest

As we build out our security model this will enable composable sandboxing. Handles are thread-local, can be duplicated with fewer rights, and can be transferred on purpose. A new thread can start with a streamlined set of capabilities (object handles and rights), so worker threads only get the authority they actually need.

Clear fault boundaries

Threads are the unit of fault isolation. Threads do not get broad access by default and they exchange data through message passing and explicit handle transfer. This keeps failures easier to contain and makes it clearer which part of the program is allowed to do what.

A narrow ABI

The ABI is intentionally small. Guest code enters through pb_thread, then talks to the runtime through pb_syscall. Each call carries an opcode, a subject handle, and integer arguments. That keeps the boundary stable, inspectable, and practical to host on native and web targets.

Syscalls and events

The syscall surface looks like a small OS kernel: create channels, start threads, read and write streams, sample clocks, manage windows, observe signals. Async work is poll-based. The runtime does not push callbacks into guest code; threads call PBSysEventPoll to retrieve events when they are ready.