Authoring

The manifest

Every field, what it is for, and the one that cannot change after you publish.

addon.json sits next to your entry file. The schema that validates it is the same module CI, the dev server, and the loader all use, so a manifest that passes pnpm validate cannot fail at install.

A minimal manifest

Seven required fields. Everything else is optional.

addon.json
{
  "id": "my-addon",
  "name": "My Addon",
  "version": "1.0.0",
  "apiVersion": 1,
  "author": "you",
  "description": "What it does, in one line.",
  "entry": "main.js"
}

Fields

Field Required Notes
id yes Lower-case kebab-case, and the same as your directory name. It is your storage namespace and your keybind scope, so it cannot change once published: a rename orphans every installed player's settings, keybinds and window position, and shows in Browse as a different addon installing alongside the old one. Get it right before anyone has it.
name yes What Browse shows and what the install confirmation repeats. Unlike the id, this is free to change.
version yes Semver. A marketplace serves one version per ref, so this is what an update compares against.
apiVersion yes The addon API major, currently 1. A loader that cannot honour it marks the addon incompatible and never evaluates it, rather than running it and failing somewhere unhelpful.
author yes Shown on every row and on the install confirmation.
description yes One line. It is what Browse shows and what the install confirmation repeats, so write it for someone deciding whether to trust you.
entry yes A relative path inside your directory, usually main.js. It must not traverse outside, and the file is evaluated as a function body: no exports, no registration call, woc already in scope.
icon no A relative path to an image inside your directory.
homepage no A URL shown on the addon row.
tags no Up to six, same shape as an id. They become the filter controls in Browse, which is why they are bounded and why two authors cannot publish Combat and combat as different tags.
gameVersion no A semver range, for example ">=0.31.0". Outside it the addon is marked incompatible rather than left to break, which is the difference between a clear message and a mystery.
channels no Restrict to some of live, pbe, pbe2. Omit it unless your addon genuinely depends on something only one deployment has.
permissions no What you use, out of net.read, world.read, ui, sound, keys, storage. Shown one line each on the install confirmation. This is a disclosure, not a boundary: see below.
keybinds no { id, label, default } each. You can only bind an id you declared, and the manager renders the editor and the conflict warnings for you.
settings no boolean, number (with optional min and max), string, or select (with options). The manager renders the form; you read woc.settings and hear about changes through woc.onSettingsChange.

The id is the one you cannot take back

Everything else on that table is free to change between versions. The id is not, and it is worth understanding why rather than just obeying it.

The id is your storage namespace, your keybind scope, and half of every fully-qualified id the loader uses to tell your addon apart from someone else's with the same name. Renaming a published addon is therefore not a rename. Every player who installed it keeps their settings, keybinds and window position filed under the old name, where nothing will ever read them again, and the new name arrives in Browse looking like a different addon that installs alongside the old one.

combat-meter was called dps-meter until a healing tab made the name wrong. That rename was free, and it was free only because nothing had been released yet.

permissions is a disclosure, not a boundary

This is the most important sentence on this page, and it is easy to read the field the wrong way round.

Addon code runs in the page realm with the page's globals in scope. A manifest that declares nothing is not thereby prevented from doing anything. The list you write is what you are telling the player your addon is for, and the loader shows it on the install confirmation next to a sentence saying exactly that.

So declare what you use and nothing more, because the value of the list is that it is honest. A permission list presented with nothing beside it reads as a sandbox, and there is not one.

Settings and keybinds are rendered for you

Declare them and the manager builds the form, the keybind editor, and the conflict warnings. You never draw any of that.

{
  "settings": [
    { "id": "max-rows", "type": "number", "label": "Rows to show", "default": 10, "min": 3, "max": 40 },
    { "id": "show-detail", "type": "boolean", "label": "Show per-hit detail", "default": true }
  ],
  "keybinds": [{ "id": "toggle", "label": "Show or hide the meter", "default": "Alt+KeyD" }]
}

Both are hydrated before your first line runs, so woc.settings['max-rows'] is there immediately rather than arriving later. Changes reach you through woc.onSettingsChange, and a rebind moves your live binding for you.

You can only bind an id you declared. That is what makes the editor able to list your keys before your addon has run.

Checking it

pnpm validate

Runs the real schema over every addons/*/addon.json. The dev server runs the same reader on every request, so a manifest saved mid-session is visible on the next refresh and the dev index cannot diverge from what CI would accept.