created
2026-09-27
updated
2026-10-30

Static apps

Static apps

A static app is a small browser program stored in a vault at apps/<name>/index.html. It opens as a normal, top-level HTML page with a stable /apps/<name>/ address. Unlike an HTML embed, it is not placed inside a note or an iframe.

Create an app

Use a lowercase URL segment for <name>: it must match [a-z0-9][a-z0-9-]{0,62}. Put the entry page at the root of its directory. The smallest app can be one self-contained HTML file:

apps/
  counter/
    index.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Counter</title>
</head>
<body>
  <main>
    <h1>Counter</h1>
    <p>Count: <output id="count" aria-live="polite">0</output></p>
    <button id="increment" type="button">Increment</button>
    <button id="reset" type="button">Reset</button>
  </main>
  <script>
    let count = 0;
    const output = document.querySelector("#count");
    document.querySelector("#increment").addEventListener("click", () => {
      count += 1;
      output.textContent = String(count);
    });
    document.querySelector("#reset").addEventListener("click", () => {
      count = 0;
      output.textContent = "0";
    });
  </script>
</body>
</html>

There is no manifest, build step, or backend. You can keep scripts, stylesheets, and assets beside index.html; refer to them with paths such as ./app.js, ./style.css, and ./images/logo.svg. The app listener serves files only from that app directory.

Link to an app from a note with either form:

[Open the counter](/apps/counter/)
[Open the counter](apps/counter/)

The second form is recognized as a vault-relative app link, not resolved as a note. Query parameters and fragments are kept when the app opens.

Try the runnable demo: Open the counter. It is a self-contained counter with Increment and Reset controls; reloading starts it at zero. The code above shows the same app, with no build step or external dependency.

Open apps in the web workspace

Run track web as usual. The workspace owns /apps/<name>/ and redirects it to a second, static-only listener in the same process on the workspace port plus one. For example, a workspace bound to 127.0.0.1:8765 opens apps on port 8766. The redirect hostname comes from the bind address, not an alias used to reach the workspace: localhost stays localhost, a numeric loopback address stays numeric, and an explicit hostname is preserved. A wildcard workspace bind uses 127.0.0.1 for apps.

The adjacent port is deliberate. Startup fails if it is unavailable, or if the workspace port is 65535; the server does not silently choose a different port. If the workspace asks the OS for port 0, the app port is one above the actual workspace port, so it may change between runs.

In a multi-vault workspace, a link in a note from a registered vault opens the app with that vault selected. The workspace carries the vault choice through its redirect and removes its private selector before the app receives the query string. A direct /apps/<name>/ URL without a vault selector uses the launch vault. Each vault that needs an app must have its own apps/<name>/index.html.

Publish apps with a static site

Apps are independent of note selection and are not published by default. Explicitly allowlist each app with repeatable --app flags or comma-separated names:

track export-site --all --app counter --app dashboard \
  --frontend ./web/dist-static --out ./site

# The same allowlist in one flag:
track export-site --all --app counter,dashboard \
  --frontend ./web/dist-static --out ./site

Build the static-mode frontend before calling export-site; --frontend and --out are required. In this repository, make site runs the full help-site sequence: it type-checks and builds the static frontend, builds the CLI, exports the docs/help vault, and prerenders the result. For another vault or an app allowlist, run export-site with the desired --app flags and a static-mode frontend build. See CLI for note selection and the other site options.

Every selected app must have a regular index.html. Export copies its files byte-for-byte to <out>/apps/<name>/, keeping their names and relative layout; missing apps, invalid names, and symlinks that leave the app fail the build. Each rebuild replaces the output apps/ tree, so an app omitted from the new allowlist is removed rather than left from an earlier build. Links to /apps/<name>/ and apps/<name>/ resolve under the site base path automatically.

--apps-base-url optionally routes app links to an absolute HTTP(S) origin/path prefix for a separate apps deployment. For example, --apps-base-url https://tools.example/base makes the app route start at https://tools.example/base/apps/. This does not upload the app: export still copies selected files into <out>/apps/, and you must deploy those files yourself if the links point to another host. This setting is independent of --base-url, which describes the note site for canonical and social URLs.

Trust and limits

Static apps are for trusted, self-authored code, not arbitrary or untrusted content. An app opens as a top-level page with no iframe and no sandbox, so it can use normal browser capabilities, including network access. The app listener has no Track API, vault-file route, directory listing, or single-page-app fallback; missing files return 404. Track API integration is unsupported. Use relative paths for files inside the app.

Apps share one origin across every app and vault served by the workspace. localStorage is shared across that origin too, including between apps, so namespace stored keys by app and by an app-owned stable vault identifier. Published apps on the same origin also share origin-scoped storage with the published site. Ports do not isolate cookies: cookies are not scoped by port, even though the live app listener uses a different port from the workspace.

Do not use the workspace internal launch selector as a persistent vault ID: it aliases the vault the workspace was started in and can refer to a different vault after a restart.

For note HTML kept under assets/ and shown inside a sandboxed frame, use Embeds. For app routes, live serving, and multi-vault behavior, see Web workspace; for publishing, see CLI.