Skip to content

What You Can Build#

Jac compiles one language to three runtimes -- Python bytecode (server, sv), JavaScript (client, cl), and native machine code (na, which also compiles to in-browser WebAssembly) -- so the same skills produce a CLI tool, a REST API, a full-stack app, a desktop/mobile build, native compute that runs in the browser, or a C-callable shared library. This page is a cookbook: a small, working example of each common thing you can build with Jac today, plus the verbs that build and run it. Each one is a combination of a few building blocks, not a separate mode.

Every example below was run against the current toolchain. Install once and follow along:

curl -fsSL https://raw.githubusercontent.com/jaseci-labs/jaseci/main/scripts/install.sh | bash

This installs the self-contained jac binary -- no Python, pip, or uv required.

jac run is kind-aware

Set kind under [project] in jac.toml (or let it be inferred from the entry-point's codespace), and a bare jac run does the right thing for that kind: execute runnable kinds (cli, cli-native), serve server kinds (service, web-app, ...), or build artifact kinds (native-binary, native-lib, py-package, js-package). jac run --show prints the resolved plan and the equivalent primitive command without running it. The explicit verbs shown in each recipe below are those primitives.

The recipes at a glance#

Jac gives you three runtime targets -- server (sv), client (cl), and native (na) -- plus a few ways to serve, package, or wrap them in a shell. Everything below is a combination of those building blocks, not a separate mode. The grid shows which blocks each recipe uses; each recipe's exact command is in its section below.

Jac is also batteries-included -- it bundles LLVM, ships its own native linker, runs its own server, and auto-installs the JS runtime (bun) on demand. The only recipes needing an external toolchain are the ones wrapping a native OS shell, called out in the last column.

Each recipe name links to its guided "I like to build…" track -- a 5-minute quick win plus a curated path through the tutorials and reference. The detailed inline recipe for each is in the sections further down this page.

Recipe status sv cl na served packaged shell requires
CLI tool --
Native CLI tool --
Native binary --
API service --
Microservices ● ×N --
Python package (PyPI) wheel twine¹
npm package (npmjs.com) npm npm³
Shared library (C ABI) .so/.dll --
Full-stack app --
Static / in-browser app --
Desktop app 🧪⁴ desktop WebKit²
Mobile app (webview) 🧪⁵ mobile Android SDK / Xcode
Mobile app (React Native) 🧪⁶ react-native Android SDK / Xcode
Full-stack package 🚧 attach --

Legend -- ● uses this block · ◐ talks to a remote server (doesn't bundle one) · ×N replicated per service. status: ✅ shipping · 🧪 beta (works, with caveats footnoted below) · 🚧 not yet wired end-to-end (see roadmap). Columns 2–7 are composition (what it's made of): sv / cl / na = which runtimes compile (na to a host binary, or to WebAssembly for in-browser native) · served = hosted by jac start (exposing any sv walkers/functions as a REST API) · packaged = produces a distributable artifact · shell = wrapped in a native desktop/mobile shell. The requires column is a different axis -- setup cost: toolchains you install yourself, excluding the built-in scale subsystem (which ships with jaclang core; its optional deploy deps are pulled per-project via [scale.*] config + jac install) and the full-stack client/desktop framework (which also ships with jaclang core).

¹ Only to upload to PyPI; jac build --as wheel itself needs nothing.   ² The desktop target ships with jaclang core (no Rust); it embeds the OS webview. On Linux you need the WebKitGTK system libraries (a bundled helper script installs them).   ³ Only to publish (npm publish); jac build --as npm builds the .tgz with no Node/npm.   ⁴ The binary renders your cl UI today; wiring sv walkers onto the embedded interpreter, HMR dev mode, and per-OS installers are in progress (#6436).   ⁵ Frontend-only Capacitor wrapper -- the app talks to a Jac server you deploy separately.   ⁶ Beta React Native (Expo/Metro) frontend built from a mobUI source tree (@jac/mobui primitives, no HTML) that also compiles for the web; it talks to a Jac server you deploy separately.

Read across a row and the composition is the point: a full-stack app is just a service plus a client; in-browser native swaps the server for an na module compiled to wasm; a desktop app is a full-stack app plus a shell; microservices are a service replicated. The 🚧 rows aren't missing "kinds" -- they're capability combinations that aren't wired yet.

Ship it: one file or one executable#

Whatever you build, two universal projections turn it into something you can hand to someone else.

A sealed app bundle (.jab) -- a bare jac build type-checks the whole project (fail-closed) and emits one self-describing .jab: client dist, serve manifest, and native binaries baked in and hash-verified. Any machine with Jac installed runs or serves it with zero live compilation, kind-aware:

jac build                  # -> dist/<app>.jab
jac run dist/<app>.jab     # cli kinds execute
jac start dist/<app>.jab   # servable kinds production-serve

A self-contained executable -- jac build --as binary appends that same sealed .jab onto a copy of the jac launcher, producing one file that carries the full runtime. Hand it to a machine with no Jac, no Python, no Node:

jac build --as binary      # -> one executable, full runtime included

How is --as binary different from the Native binary recipe below? --as native compiles the restricted na subset through LLVM into a small, dependency-free binary. --as binary packages any app -- walkers, Python imports, a full web client -- with the runtime included; the trade is a larger file. Details and the other projections (wheel, npm, source): jac build.


Backend & CLI#

CLI tool#

The simplest project: anything you run straight from the terminal -- scripts, automation, dev tools. A .jac file runs directly with the whole language and ecosystem available (it just needs Jac installed; to ship a self-contained binary instead, see Native binary). Jac is graph-native, so even a one-off script can model data as nodes and traverse them with a walker.

# hello.jac
node Person {
    has name: str;
}

walker Greeter {
    can start with Root entry {
        visit [-->];
    }
    can greet with Person entry {
        print(f"Hello, {here.name}!");
        visit [-->];
    }
}

with entry {
    root ++> Person(name="Ada");
    root ++> Person(name="Alan");
    root spawn Greeter();
}
jac run hello.jac
Hello, Ada!
Hello, Alan!

root persists

The graph hanging off root is automatically saved between runs. Run it twice and you'll see the people accumulate -- that persistence is the same machinery that backs Jac servers, with no database to set up.

Full tutorial: Jac Fundamentals · Graphs & Walkers

Native binary#

A .na.jac file compiles, through LLVM, to a standalone, zero-dependency executable you can ship to machines that have neither Jac nor Python installed -- like a curl-style single-binary tool. (Same command-line territory as a CLI tool, but the trade is reversed: ship-anywhere portability in exchange for the restricted native subset.) That subset requires a with entry block and allows no walkers/nodes/async and no Python imports.

# sum.na.jac
def compute_sum(n: int) -> int {
    total: int = 0;
    i: int = 1;
    while i <= n {
        total = total + i;
        i = i + 1;
    }
    return total;
}

with entry {
    result = compute_sum(10);
    print(f"Sum of 1 to 10: {result}");
}
jac nacompile sum.na.jac -o sum
./sum
Sum of 1 to 10: 55

The result is a real native binary (a few KB here) you can ship without Python or Jac installed. To ship a full app (walkers, Python imports, a web client) as one executable instead, see Ship it.

Full tutorial: Build a Chess Engine · Reference: Native pathway

API service#

A server with no frontend. Mark a walker walker:pub (or a function def:pub) and it becomes a REST endpoint automatically -- request bodies map onto the walker's has fields, and report becomes the JSON response.

# api.jac
node Task {
    has title: str;
    has done: bool = False;
}

walker:pub add_task {
    has title: str;
    can create with Root entry {
        task = Task(title=self.title);
        root ++> task;
        report {"id": jid(task), "title": task.title};
    }
}

walker:pub list_tasks {
    can fetch with Root entry {
        report [{"id": jid(t), "title": t.title, "done": t.done}
                for t in [-->][?:Task]];
    }
}
jac start api.jac --no-client

--no-client skips all frontend bundling -- a pure JSON API. Walkers are exposed at POST /walker/<name>:

curl -X POST http://localhost:8000/walker/add_task \
  -H "Content-Type: application/json" -d '{"title": "Write docs"}'

curl -X POST http://localhost:8000/walker/list_tasks

Interactive API docs are served at http://localhost:8000/docs (Swagger) and a live graph view at http://localhost:8000/graph.

Full tutorial: Local API Server

Microservices#

The same code runs as a monolith or as several independently-deployed services -- the only change is the sv import keyword. When both modules are server-context, the compiler turns the import into an HTTP client stub: calls become RPCs, but the source still reads like a normal import.

# math_service.jac  (the provider)
def:pub add(a: int, b: int) -> int {
    return a + b;
}

def:pub multiply(a: int, b: int) -> int {
    return a * b;
}
# calculator_service.jac  (the consumer)
sv import from math_service { add, multiply }

def:pub dot_product(a: list[int], b: list[int]) -> int {
    result = 0;
    for i in range(len(a)) {
        result = add(result, multiply(a[i], b[i]));  # each call is a POST over HTTP
    }
    return result;
}

With a jac.toml in the directory, one command brings up the whole cluster -- the consumer auto-starts every service it imports from:

jac start calculator_service.jac --port 8002

curl -X POST http://localhost:8002/function/dot_product \
  -H "Content-Type: application/json" -d '{"a": [1,2,3], "b": [4,5,6]}'

To split services across hosts, point each consumer at its providers with JAC_SV_<MODULE>_URL environment variables -- no source change. jac setup microservice --add <file> records which files become services for production deploys.

Full tutorial: Microservices with sv import

Python package (PyPI)#

A reusable library -- no entry point -- packaged as a standard pip wheel. Any def:pub is part of the public API.

# greetlib.jac
def:pub greet(name: str) -> str {
    return f"Hello, {name}!";
}
# jac.toml
[project]
name = "greetlib"
version = "0.1.0"
description = "A tiny Jac library"
jac build --as wheel
# → dist/greetlib-0.1.0-py3-none-any.whl

Upload it with twine, then pip install greetlib anywhere. The wheel ships your compiled modules and runs under the jac binary -- it does not list jaclang as a runtime dependency.

Reference: Publishing

npm package#

The client-side counterpart to the Python package: a cl component (or function) library published to npm so any JavaScript or TypeScript project can npm install it -- whether or not they use Jac. The same jac.toml drives it; jac build --as npm compiles your client modules to ES-module JavaScript, generates package.json, and emits .d.ts TypeScript declarations.

# greetui/index.cl.jac
def:pub Greeting(name: str) -> JsxElement {
    return <h1>Hello, {name}!</h1>;
}
# jac.toml
[project]
name = "greetui"
version = "0.1.0"
description = "A tiny Jac component library"

[project.include]
packages = ["greetui"]

[npm]
name = "@myscope/greetui"   # optional scoped npm name
jac build --as npm
# → dist/myscope-greetui-0.1.0.tgz   (run jac build --as wheel to build the wheel too)

The generated package.json wires in @jaseci/runtime automatically for JSX/reactive code. Upload it with npm publish (Jac builds the tarball but doesn't upload, exactly like twine for wheels).

npm packages must be standalone client code

A module that crosses a server boundary (an sv import or call) can't run from a plain npm install, so jac build --as npm rejects it with a clear error. Keep server-coupled code in your app, not in the published library.

Reference: Publishing to npm

Shared library (C ABI)#

The native counterpart to the Python and npm packages: an na module compiled to a C-ABI shared library (.so / .dylib / .dll) that any language with a C FFI -- C, C++, Rust, Go (cgo), Python (ctypes) -- can link or dlopen. It's the mirror image of import from "lib.so" (calling C from Jac): here you expose Jac to C. Like the other packages it has no entry point; the public surface is whatever you mark :pub.

# mathlib.na.jac
glob:pub counter: int = 7;                  # exported global

def:pub jadd(a: int, b: int) -> int {       # exported function
    return a + b;
}

obj:pub Point {
    has x: int = 0, y: int = 0;
}

def:pub make_point(x: int, y: int) -> Point { return Point(x=x, y=y); }
def:pub point_sum(p: Point) -> int { return p.x + p.y; }
jac nacompile mathlib.na.jac --shared                    # → ./libmathlib.so
jac nacompile mathlib.na.jac --shared --target macos     # → ./libmathlib.dylib
jac nacompile mathlib.na.jac --shared --target windows   # → ./libmathlib.dll

Load it like any other shared library -- here from Python via ctypes:

import ctypes
lib = ctypes.CDLL("./libmathlib.so")
lib.jadd.restype = ctypes.c_int64
lib.jadd.argtypes = [ctypes.c_int64, ctypes.c_int64]
print(lib.jadd(2, 3))   # 5

Scalars pass by value; Jac objects and strings cross as opaque handles (a void* you hand back to the library), with exported jac_retain/jac_release to manage their reference-counted lifetime, and module globals initialize automatically on load. Same batteries-included story as the rest -- Jac's own linker emits the ELF/Mach-O/PE file, so there's no gcc, ld, or lld in the loop (and the --target cross-builds need no extra toolchain either).

Reference: Native pathway -- Shared libraries


Full-stack & apps#

Full-stack app#

The headline case: backend, frontend, and data model in one file. Code in a cl block (or .cl.jac file) compiles to a React/JSX bundle for the browser; everything else compiles to Python for the server. The compiler generates the HTTP calls between them -- await add_todo(...) in the client is a real RPC to the server function, with types shared across the boundary.

# main.jac
node Todo {
    has title: str, done: bool = False;
}

def:pub add_todo(title: str) -> Todo {
    todo = Todo(title=title);
    root ++> todo;
    return todo;
}

def:pub get_todos -> list[Todo] {
    return [root-->][?:Todo];
}

cl def:pub app -> JsxElement {
    has todos: list[Todo] = [], text: str = "";
    async can with entry { todos = await get_todos(); }
    async def add {
        if text.strip() {
            todos = todos + [await add_todo(text.strip())];
            text = "";
        }
    }
    return <div>
        <input value={text}
            onChange={lambda e: ChangeEvent { text = e.target.value; }}
            placeholder="Add a todo..." />
        <button onClick={add}>Add</button>
        {[<p key={jid(t)}>{t.title}</p> for t in todos]}
    </div>;
}
# jac.toml
[project]
name = "mini-todo"

[dependencies.npm]
react = "^18.2.0"
react-dom = "^18.2.0"

[dependencies.npm.dev]
vite = "^6.4.1"
"@vitejs/plugin-react" = "^4.2.1"
typescript = "^5.3.3"
"@types/react" = "^18.2.0"
"@types/react-dom" = "^18.2.0"

[serve]
base_route_app = "app"

[client]
jac start          # production server
jac start --dev    # hot-module reload while you edit

Open http://localhost:8000. No database, no separate frontend project, no glue code.

Full tutorial: Full-Stack Project Setup

In-browser native (wasm)#

The na runtime's other target: rather than a host binary, an na {} block compiles to WebAssembly and runs in the browser, driven by a cl page -- native-speed compute (a game loop, a simulation, a hot inner loop) executing client-side with no server round-trip. It's the mirror image of a full-stack app: there the heavy lifting runs on the server (sv); here it runs in the browser (na -> wasm). The block's import from ... externs become the wasm module's imports, satisfied from JavaScript -- the same native source contract as a native binary, fulfilled by a different host.

One module holds both halves:

# main.jac
na {
    """Count primes below n -- a tight integer loop, compiled to WebAssembly."""
    def count_primes(n: int) -> int {
        count = 0;
        i = 2;
        while i < n {
            is_prime = True;
            j = 2;
            while j < i {
                if i % j == 0 { is_prime = False; break; }
                j += 1;
            }
            if is_prime { count += 1; }
            i += 1;
        }
        return count;
    }
}

cl {
    def:pub app -> JsxElement {
        has answer: str = "computing...";
        async can with entry {
            res: any = await WebAssembly.instantiateStreaming(
                fetch("/static/main.wasm"), {"env": {"puts": lambda { return 0; }}}
            );
            wasm: any = res.instance.exports;
            wasm.__jac_glob_init();
            # an i64 crosses the JS boundary as a BigInt; format it straight to text
            answer = f"{wasm.count_primes(BigInt(20000))}";
        }
        return <div>
            <h1>Native compute in the browser</h1>
            <p>{"primes below 20000 (computed in wasm): "}<b>{answer}</b></p>
        </div>;
    }
}

It uses the same jac.toml as the full-stack app (React deps + [client]).

Set kind = "web-static" in jac.toml so the toolchain treats it as a client-only app (no backend):

jac start          # builds the cl bundle + na->wasm, serves on http://localhost:8000
jac start --dev    # same, with hot reload
jac build          # portable, self-contained dist in .jac/client/dist/

Because a client project has no server, jac start serves the build with a minimal static server (no API server, auth, or database) and jac build emits a portable index.html with its JS/CSS inlined, so a pure cl page opens directly from disk (file://). An app that fetches /static/main.wasm at runtime, like this one, must be served (the browser can't fetch the module over file://). See Client-only apps.

jac start compiles the na block to /static/main.wasm as part of the client build -- no emscripten and no wasm-ld; Jac's own WebAssembly linker turns the object into an instantiable module -- and the page fetches it on mount. Open http://localhost:8000:

primes below 20000 (computed in wasm): 2262

The boundary is the raw wasm ABI

A cl page drives the module through the WebAssembly interface directly -- instantiateStreaming, exports, and C-ABI value marshalling (an int / i64 arrives in JavaScript as a BigInt). Wrapping that glue in a reusable .cl.jac keeps the page clean: the full example below does exactly this with a WebGL shim that fulfills a graphics module's import from raylib externs in the browser.

Full example: raylib cube shooter (web) · Reference: Native pathway

Desktop app#

Wrap the same full-stack app in a native desktop window. Jac compiles your cl UI into one jac nacompiled binary that embeds the OS webview (WebKitGTK / WKWebView / WebView2) - no Rust toolchain, no PyInstaller, no separate process.

The desktop target ships with jaclang core -- no separate install or setup step.

jac build --client desktop            # -> .jac/client/desktop/<app>  (single binary)
jac start --client desktop            # build + launch the native window

Window title and size are configured under [desktop] in jac.toml.

Full tutorial: Desktop App

Mobile app (webview)#

Ship the same client bundle to Android/iOS via Capacitor, which wraps it in a native webview. The mobile app is the frontend only -- it talks to your Jac server over HTTP, so deploy the backend separately (e.g. as an API service).

# prerequisites: Android: JDK + Android SDK; iOS (macOS): Xcode (no Node.js -- JS tooling runs on the bundled Bun)
jac setup mobile --platform android    # one-time scaffold (android/)

jac start main.jac --client mobile --dev          # live reload on device/emulator
jac build --client mobile --platform android      # → android/.../app-debug.apk

Use --platform ios on macOS to produce an Xcode project. App name and id are set under [client.mobile].

Full tutorial: Mobile App

Mobile app (React Native)#

Ship a true native mobile app (Android + iOS) using React Native, with platform-native views rather than a webview. This is the frontend only -- it talks to your Jac server over HTTP, so deploy the backend separately (e.g. as an API service).

A React Native app is a mobUI project: one source tree that compiles to both web (via react-native-web) and native (Android/iOS via Metro). Instead of HTML tags, mobUI projects use Jac's @jac/mobui component vocabulary (View, Text, Pressable, TextInput, Image, ScrollView), which projects to every target. Raw HTML tags (<div>, <span>, ...) are compile errors in a mobUI project -- see E1105.

# prerequisites: Android: JDK + Android SDK; iOS (macOS): Xcode (no Node.js -- JS tooling runs on the bundled Bun)
jac setup react-native              # one-time scaffold (.jac/mobile-rn/)

jac start main.jac --client react-native --dev   # Fast Refresh on device/emulator
jac build --client react-native --platform android
jac build --client react-native --platform ios    # macOS only

Set client_kind = "mobui" under [project] in jac.toml to opt in. The scaffold and build options live under [client.react_native].

Full reference: React Native target · Tutorial: Mobile App


On the roadmap#

These aren't missing "kinds" -- they're capability combinations that aren't wired end-to-end yet. Here's the honest status and the closest thing you can do today.

  • Full-stack package (sv + cl + attach) -- An installable feature that brings its own routes, UI components, and data models into your app (think "drop in payments and get a checkout button + endpoints + models"). sv import composes services over HTTP, but there's no attachable in-process package yet. This needs a no-entry "package" artifact and conflict-resolution semantics across the three runtimes.

Want to follow the design?

The unified jac build verb and the sealed .jab artifact have shipped (see Ship it). What remains for this row is the attachable package form: a no-entry .jab a host app can mount, plus its conflict-resolution semantics, tracked in the Jac repo's proposals and issues.