Skip to content

tinyjs.json ​

every key is optional except name

tinyjs.json sits at the root of the project and describes the app: its name and id, how it's laid out and built, its windows, and how it plugs into the OS. tinyjs dev, build, publish and notarize read it. A new project starts with:

json
{
  "$schema": "https://tinyjs.app/schema/tinyjs.schema.json",
  "name": "myapp",
  "title": "My App",
  "size": "960x640",
  "id": "com.example.myapp",
  "version": "0.1.0"
}

The $schema line gives editors completion and type checks for every key below — see editor schema. Keys that need different values per OS go in a macos / windows / linux block, and two signing keys can come from environment variables instead.

the app ​

name ​

string · required

The executable's name: dist/<name>, <name>.exe, and the name in dmg and zip files. Also the default for title and id.

title ​

string · default: name

What people see: the window title, the menu bar's app name, the .app folder (dist/<title>.app), the dmg's volume name, and the app's name in Linux and Windows file associations.

id ​

string · default: com.example.<name>

A reverse-domain identifier, unique to your app (com.yourcompany.myapp). The OS uses it to tell apps apart — it's the macOS bundle identifier — and tinyjs keys the app's own storage to it: the data folder, store, secrets, saved window positions, and single-instance detection. Changing it after release starts users over with empty settings, so pick it before you ship.

version ​

string, like "1.2.0"

Shown in the About panel and returned by tiny.app.info(). tinyjs publish requires it, and auto-update compares it (the first major.minor.patch) to decide whether a release is newer.

minTinyjsVersion ​

string, like "0.62.0"

The oldest tinyjs this project works with. An older tinyjs refuses to dev or build it and says to run tinyjs update, instead of half-working with calls that do nothing. tinyjs new and tinyjs wrap fill it in with the version that made the project.

$schema ​

string

The JSON Schema URL for editors. tinyjs ignores it; tinyjs doctor notes when it's missing.

code and build ​

backend ​

string path · default: the first of src/backend/main.js|ts, src/main.js|ts, backend/main.js|ts

The backend entry file. Its folder is the backend's root: a .js backend ships that whole folder, and a .ts entry is bundled from it with esbuild (so npm packages work). Code both sides use goes in a shared folder beside it. tinyjs dev restarts the backend when anything in that folder changes.

frontend ​

object

Where the page comes from. A plain project needs nothing here: the page is src/frontend/, served as-is.

json
{ "frontend": { "dir": "web" } }

A bundler project (what tinyjs new --template writes) gives build and dev commands instead:

json
{
  "frontend": {
    "build": "npm run build",
    "dist": "dist",
    "dev": "npm run dev",
    "devUrl": "http://127.0.0.1:5173"
  }
}
field
dirthe plain page folder, default src/frontend. It needs an index.html (unless the app has no main window or uses url).
builda shell command tinyjs build runs first; its output ships instead of dir. The project's packages must be installed.
distwhere build writes, default dist.
deva shell command tinyjs dev starts — the dev server. Used only with devUrl.
devUrlthe dev server's address. tinyjs dev waits for it to answer, then loads it, and the server's own hot reload takes over. Leave dev out to run the server yourself. Dev only; builds ship dist.
reloadfalse: tinyjs dev doesn't reload the page when files change, for a watcher of your own that does.

inject ​

string path

A script run at document start in every window and every page, before the page's own scripts — for polyfills, or for adjusting a wrapped site. A .ts file is bundled with esbuild.

icon ​

string path · default: assets/icon.png

The app icon, a 1024×1024 PNG. macOS turns it into AppIcon.icns; Windows stamps it into <name>.exe; Linux ships it as icon.png and uses it for the app's desktop entry. See assets.

resources ​

string[] · default: everything in resources/

Which files and folders inside resources/ to ship. Omit it to ship the whole folder; [] ships none. See resources.

executables ​

string[] · default: the right files from executables/ for the OS and CPU being built

Which programs inside executables/ to ship. Each must be a native program for the target OS. [] ships none. See executables.

executableEntitlements ​

object · macOS

Entitlements for a sidecar when the app is signed with a Developer ID, by file name:

json
{ "executableEntitlements": { "ffmpeg": ["com.apple.security.cs.disable-library-validation"] } }

Sidecars not listed keep the entitlements they were built with. See executables.

extras ​

string[] · default: the top level of extras/, plus extras/<os>/

Files for the person downloading the app — a README, a licence — placed beside it: in the Windows and Linux folder and archive, and at the root of the macOS dmg (never inside the app). [] ships none. See extras.

helpBook ​

string · macOS

A help book folder inside assets/ (for example "My App Help"), so the Help menu opens it. Set it in the macos block. See assets.

windows ​

size ​

string · default: "960x640"

The main window's starting size, "WIDTHxHEIGHT" — the page area, not counting the title bar. See tiny.win.

mainWindow ​

boolean · default: true

false starts the app with no main window and no page at all — a tray or background agent that opens windows when it needs them. Can't be combined with url.

chrome ​

object

How the main window looks before the page draws anything: frameless, translucent, custom title-bar controls. Same fields as tiny.win.setChrome(), which changes them later.

json
{ "chrome": { "frame": false, "vibrancy": "hud", "windowControls": ["close"] } }
fieldmacOSWindowsLinux
framefalse: no title bar or border — make your own with data-tiny-drag✓✓✓
windowControlstrue / false, or a list of "close" "minimize" "maximize"✓close always shows with the othersbest effort
transparentsee-through window background✓main window, set here only✓
vibrancyblurred background material, such as "hud" "sidebar" "popover"; false for none✓Windows 11: Acrylic for hud, popover, menu; Mica otherwise—
squareCornersno rounded corners✓✓✓
acceptsFirstMousea click on an inactive window also acts on what it hits✓——
windowControlsPos{ "x": 12, "y": 14 }: where the traffic lights sit✓——
menufalse: no menu bar in this window— (one menu bar per app)✓✓
panela non-activating panel: takes the keyboard without activating the app, so it can open over full-screen apps — see window fx✓——

rememberWindows ​

boolean · default: false

Put every window back where the user left it — position, size, maximized or full screen — remembered separately for each set of plugged-in screens. The main window stays hidden until it's been placed, so it doesn't jump. See tiny.win.

offscreenRescue ​

boolean · default: true

When the screens have changed since the last run (a display unplugged), windows that would open where nobody can see them are moved onto a screen. false turns that off, for apps that park windows off-screen on purpose. tiny.win.ensureOnScreen() works either way.

windowPlacement ​

boolean · Linux

true if the app places its own windows (setPosition, center). Wayland doesn't let apps do that, so on Linux this runs the app through X11 (XWayland) when it's available. Does nothing on macOS and Windows.

activation ​

"regular" (default) or "accessory"

"accessory" makes a menu-bar style app: no Dock icon on macOS, no taskbar button on Windows and Linux, and the window starts hidden until the app shows it — pair it with a tray icon.

behaviour ​

contextMenu ​

boolean · default: true

false hides the webview's built-in right-click menu (Reload, Inspect…). A custom context menu still shows.

debug ​

true, "open" or false (default)

Developer tools in a built app: true lets F12 open them (⌥⌘I on macOS too), "open" opens them with every window. tinyjs dev always has F12.

browserAccelerators ​

boolean · default: false · Windows

true keeps WebView2's own keyboard shortcuts (Ctrl+P print, Ctrl+F find, Ctrl+R reload…). They're off by default so they can't fire underneath your app's own shortcuts.

about ​

"menu" · macOS

The app menu's About item sends onMenu('about') and the page's menu event instead of showing the standard About panel, so you can show your own. about becomes a reserved menu item id. See menus.

attribution ​

string · macOS · default: "Made with tinyjs"

The credit line in the standard About panel. An assets/Credits.html (or .rtf) replaces it with formatted credits — see assets.

downloads ​

"auto" (default), "ask" or "deny"

What happens when the page starts a download: auto saves to the Downloads folder, ask shows a save dialog, deny refuses. The backend hears each download's progress through onDownload, but can't change the decision — see site wrappers.

popups ​

"external" (default), "window" or "deny"

What window.open and target="_blank" links do: external opens them in the default browser, window in a new app window (keeping window.opener, for sign-in popups), deny blocks them. The backend can decide per popup with onWindowOpen — see site wrappers.

userAgent ​

string

Replaces the webview's user agent, for sites that sniff it.

readAccess ​

true or a folder path · macOS

Lets the page load local files from outside its own folder — your home folder with true, or one folder — through tiny.fileURL(path). Windows and Linux already allow it.

OS integration ​

These are registered for built apps only — part of the .app on macOS, added when the app first runs on Windows and Linux — never by tinyjs dev. See deep links & files.

urlScheme ​

string or string[]

Links like myapp://… open the app and arrive in onOpenUrl. A list registers several schemes.

fileExtensions ​

string[], like ["md", "txt"]

File types the app can open: they're listed under "Open With", and opened files arrive in onOpenFiles. Windows adds the app to "Open with" without making it the default.

openFolders ​

boolean · macOS, Linux

Folders can be opened with the app too (dropped on its icon, or "Open With"); they arrive in onOpenFiles.

permissions and audio ​

permissions ​

object

Camera, microphone, speech recognition and location, each with the reason shown to the user:

json
{ "permissions": { "microphone": "To record voice notes", "camera": "For video calls" } }
  • macOS: the reasons appear in the system permission prompts, and declaring a key is what lets the app ask at all. speechRecognition is macOS only and needs microphone too. location is macOS only: it lets the app ask with permissions.request('location'), which system.wifi() needs to read the network name. Signed builds get the matching hardened-runtime entitlement.
  • Linux: microphone and camera decide whether the page may use getUserMedia at all.
  • Windows: not needed — WebView2 shows its own prompt.

See permissions.

audioTap ​

"app", "system" or off (default)

Turns on tiny.audioTap, which reads audio being played: "app" the app's own, "system" everything. Without it, audioTap.start() fails with not-declared.

audioTapReason ​

string · macOS · default: "<title> reads audio output for metering and visualization."

The reason shown in macOS's audio capture prompt.

signing and notarizing (macOS) ​

signIdentity ​

string · macOS · default: ad-hoc

The codesigning certificate, by name: "Developer ID Application: Your Name (TEAMID123)". Without it the app is signed ad-hoc — it runs on your Mac, but Gatekeeper blocks it on others. TINYJS_SIGN_IDENTITY overrides it. See notarize for setting up a certificate.

notarize ​

{ "profile": "…" } · macOS

The notarytool keychain profile tinyjs notarize submits with. TINYJS_NOTARY_PROFILE overrides it.

updates ​

update ​

object

json
{ "update": { "url": "https://example.com/myapp/manifest.json", "auto": "daily" } }
field
urlthe manifest.json tinyjs publish wrote, wherever you host it. Must be https:// (http://localhost works for testing).
auto"launch": check a few seconds after the app starts; "daily": and then once a day. A newer version fires onUpdateAvailable and the page's update-available event. Leave it out to check only when you call tiny.app.update.check(). Built apps only.
publicKeyrequire updates to be signed: the base64 public key, or a list of keys (a signing key plus a backup). See signed updates.

See auto-update.

site wrappers ​

url ​

string

Makes the main window a hosted web page instead of your own frontend — wrapping a web app as a desktop app. tinyjs wrap writes this, plus api, popups and downloads, for you. Can't be combined with mainWindow: false. See site wrappers.

api ​

string, string[] or object

Which tiny calls pages may make. Set this whenever the app loads a site you don't control: tiny is available to every page in the app, so without it a wrapped site's JavaScript could call anything your backend can do. Checked in the backend, by the calling frame's real origin.

value
omitted, "all"no limit — fine for an app that only shows its own pages
"none"no calls at all
"wrapper"window, menu, tray, dialog, notification and store calls, plus a few read-only ones — no files, processes or network. Your own api methods are off; enable them by name
["notify", "store.*"]only these
{ "preset", "enable", "disable", "origins" }start from a preset, then turn calls on and off. enable wins over disable

Names are the call's name (notify, win.setTitle), prefix.* for a group, or * for all. origins gives each site its own rules, first matching key wins (* in a key matches anything):

json
{
  "api": {
    "disable": ["*"],
    "enable": ["win.*"],
    "origins": {
      "https://app.example.com": ["notify", "store.*", "media.microphone"],
      "file://*": "all"
    }
  }
}

A page whose origin matches no key gets the top-level enable/disable lists. media.camera, media.microphone and media.proxy in an origin's list let that site use the camera or microphone without a prompt (macOS) or at all (Linux), and use tiny.proxyURL. The same rules decide which events such a page receives — your own app.push events need event.<name> (see events). An unknown preset name blocks everything, and says so. See site wrappers for the full model.

per-OS overrides ​

Root keys apply everywhere. A macos, windows or linux block is merged on top for that OS, so the keys that genuinely differ (an .ico, a macOS signing identity, a vibrancy that means nothing elsewhere) don't make you write three of everything. The block names are what tiny.system.os() returns.

jsonc
{
  "name": "myapp",
  "chrome": { "frame": false },

  // merged on top on that OS — for dev, build and publish alike
  "macos":   { "signIdentity": "Developer ID Application: …",
               "chrome": { "vibrancy": "hud" } },
  "windows": { "icon": "assets/icon.ico" },
  "linux":   { "icon": "assets/icon-512.png" }
}

Objects merge, so the macOS block above ends up with { "frame": false, "vibrancy": "hud" } rather than losing the root's frame. Everything else — strings, numbers, lists — replaces the root value.

environment variables ​

Two keys can come from the environment instead, so a signing identity or notary profile can live in your shell or CI secrets rather than a committed file:

variablesets
TINYJS_SIGN_IDENTITYsignIdentity
TINYJS_NOTARY_PROFILEnotarize.profile

They win over the file — root and OS block alike — and the build prints a line when one replaces a value the file set (==> signIdentity from TINYJS_SIGN_IDENTITY (overriding tinyjs.json)), so a variable left exported in a shell can't quietly change how a project signs. The order is root → OS block → environment. The other TINYJS_* variables are for running and debugging — see cli.