Project Model #

A Budo project is a plain folder. It holds an entrypoint file, an optional app.json, and the assets your app uses. There is no package manager, bundler, or generated source tree: Budo runs the folder as it is, on desktop, on the web, and on Android.

Entrypoints #

Budo picks the runtime from the name of the entrypoint file in the project root. It looks for these files, in this order:

  • main.ts or main.js: JavaScript, run by QuickJS. If both exist, main.ts wins.
  • main.lua: Lua 5.4.
  • main.wasm or main.wat: WebAssembly, run by Wasmtime. If both exist, main.wasm wins.

A project uses one language. If the folder contains entrypoints for two languages, for example main.js and main.lua, Budo refuses to start it.

Not every target runs every language:

  • Desktop runs JavaScript, TypeScript, Lua, and WebAssembly.
  • Web export runs JavaScript, TypeScript, and Lua.
  • Android packaging (Budo Pro) runs JavaScript, TypeScript, and Lua.

Lua and WebAssembly covers the differences between the runtimes.

Native C projects are a separate kind of project. They are trusted, unsandboxed code that you compile with budo compile, rather than main.* files that Budo runs in a sandbox. Create one with budo init my-app --language c. Native C projects can also be packaged for Android.

Project layout #

A small app is a single main.js. As it grows, a typical project looks like this:

my-app/
  app.json          app name, version, and permissions
  main.ts           entrypoint
  draw.ts           modules imported by main.ts
  state.ts
  shader.vert       GLSL shaders
  shader.frag
  assets/
    logo.svg        images and fonts
    Inter-Regular.ttf
  data/
    seed.db         a SQLite database shipped with the app
  models/
    classifier.onnx an ONNX model for sys.neural

Only the entrypoint name is fixed. Name and arrange the other folders as you like, and refer to the files by their path from the project root.

The app.json file #

app.json sits in the project root and describes your app and what it is allowed to do. It is optional; budo init creates one for you.

app.json
{
  "name": "My Budo App",
  "author": "N. Developer",
  "version": "1.0.0",
  "date": "2026-05-01",
  "orientation": "portrait",
  "network": ["api.example.com"],
  "filesystem": false,
  "neural": true
}

The fields are:

  • name, author, version, and date identify the app. Without them, Budo uses the folder name, Unknown, and 1.0, and leaves the date empty.
  • orientation is portrait, landscape, or unspecified (the default).
  • network lists the domains your app may reach with HTTP. Use "*" to allow any domain, a comma-separated string, or an array of domains. Without it, HTTP requests are refused.
  • filesystem set to true asks for access to shared storage on Android. Your app can always read its own files and write to its private storage without it, so leave it off unless you need it.
  • neural set to true enables ONNX inference with sys.neural.

SQLite databases need no permission.

Android packaging reads more fields from app.json: package identity, icons, SDK versions, Android permissions, signing, and store listing details. Packaging and deployment describes them.

File paths #

APIs that load files take paths relative to the project root: shaders, fonts, SVGs, textures, modules, and models. A path such as shaders/blur.frag works the same way on desktop, on the web, and on Android, because your project files are bundled with the app. Absolute paths and paths that climb out of the project with .. are rejected.

sys.files reads and writes files through two folders:

  • assets/ is the app's own bundle, read-only. On desktop it is the project folder, so assets/data/seed.db refers to data/seed.db in your project.
  • files/ is the app's writable storage. On desktop it is the project folder too, unless you start the app with budo run my-app --file-root <path>. On Android it is the app's private external storage, and on the web it is stored in the browser.

sys.assets is a shortcut for reading from the bundle: sys.assets.readText("config.json") is the same as sys.files.readText("assets/config.json").

main.js
// Read a settings file shipped with the app (config.json in the project root).
const defaults = sys.assets.readText("config.json");

// Save the user's settings in writable storage.
sys.files.writeText("files/settings.json", defaults);

Data, files, and network covers the file API in full.

TypeScript support #

Budo runs main.ts and imported .ts modules without a build step. Before running a file, it removes the TypeScript syntax: type annotations, interfaces, type aliases, generics, as casts, non-null assertions, access modifiers, implements, and declare. It keeps the line layout of your file, so error line numbers match your source.

This is not the TypeScript compiler. Budo does not type-check, and these constructs are not supported:

  • enum
  • namespace and module blocks
  • decorators
  • satisfies

Very dense expressions can also confuse the type remover, for example a ternary inside a function call with typed arguments. When you get an unexpected syntax error, split the expression into simpler statements. For type checking, use your editor with budo.d.ts, or run tsc --noEmit.

Capabilities #

Some features depend on the platform or the build: the web has no UDP or ONNX inference, and sensors exist mostly on phones. sys.capabilities tells your code what the current runtime supports, so it can offer a fallback instead of failing.

main.js
// Check once at startup, and keep the answers.
const hasSensors = sys.capabilities.sensors.available;
const hasMidi = sys.capabilities.midi.available;

function frame() {
  // Clear the canvas and pick a light text color.
  sys.canvas.clear("#222831");
  sys.canvas.setFillColor("#eeeeee");
  // Pick the steering method from the sensor check.
  sys.canvas.drawText(hasSensors ? "Tilt to steer" : "Use the arrow keys to steer", 24, 48, 24);
  // Report whether MIDI devices can be used.
  sys.canvas.drawText(hasMidi ? "MIDI is available" : "MIDI is not available", 24, 84, 24);
  // Keep the frame loop going.
  sys.animation.requestFrame(frame);
}

// Start the frame loop.
sys.animation.requestFrame(frame);

Each entry has an available field. The entries are neural, llamacpp, midi, udp, http, and sensors.

Runtime lifecycle #

When your app starts, Budo runs the entrypoint once, from top to bottom. Use that pass to set up state and load resources, then call sys.animation.requestFrame to start the frame loop.

On each frame, Budo updates the input state, runs pending callbacks such as completed HTTP requests and MIDI messages, calls your frame function, and shows the result on screen. Your frame function should read input, update state, draw, and request the next frame.

Keep the frame function short. While it runs, nothing else happens: no drawing, no input, no callbacks. Split long work across several frames or use the asynchronous APIs.

When nothing on screen is moving, call sys.animation.waitForInput(frame) instead of requestFrame. Budo then runs frame only after the next input event or window resize, which saves battery.