Developers

The app pack

One JSON file, at most 1 MB. Five fields are required; everything else has a default. A broken pack is refused with a message that names the field.

The smallest pack

pulsar-app.json
{
  "pulsar_app": 1,
  "id": "example-kitchen",
  "version": "1.0.0",
  "name": "Example Kitchen",
  "domains": ["example.com"]
}

This is already an app: it opens https://example.com/ in its own window, with the site’s name, a plain icon and its own login.

Plain JSON only: no comments, no trailing commas. Unknown fields are ignored, so older Pulsars read newer packs.

Every field

FieldTypeLimitsDefault
pulsar_appnumberMust be 1 (the format version). Required.—
idtext2–63 of a–z 0–9 . _ -, lower case. Required. Never change it: it is how updates find the app.—
versiontextNumbers and dots, 1–4 parts (1.4.0). Required. Every change is a higher version.—
nametextUp to 40 characters. Required.—
short_nametextHome-screen label, up to 12 characters.name cut to 12
taglinetextUp to 80 characters, shown in lists.none
descriptiontextUp to 600 characters, shown on the detail page.none
developertext or object"Your name" or {"name", "url"}; url must be https.none
start_urltexthttps, on the app’s domains. http is refused.https://<first domain>/
domainslist of text1–10 plain domains. Each covers itself and all its subdomains. Required.—
icontext or objectA Pulsar icon name (white on tile_color), an https image on your domains, or {"glyph", "url"}.a plain tile
tile_colortext#RRGGBB, the icon tile.chosen by Pulsar
theme_colortext#RRGGBB, App mode’s top bar.tile_color
displaytextstandalone (slim top bar) or fullscreen (no bar).standalone
opens_astextapp (App mode) or tab (a browser tab). People can change it.app
pluginslist of textUp to 5 Pulsar extension ids suggested inside the app (the field keeps an old name; it means extensions). Pulsar keeps the choice; extensions that are on run in apps too.none
changeslistUp to 8 lines for “What Pulsar changes”: {"title" (≤ 40), "kind": "added" | "removed" | "improved", "icon"?}.none
settingsobjectA SettingSpec, {"sections": [...]}: at most 12 sections, 40 items. See Settings.none
csslistCSS blocks. See CSS, JS and headers.none
jslistJavaScript blocks, same shape as css.none
headerslistUp to 20 request-header changes on your own pages.none
share_targetslistUp to 5 places a shared link or text can open in the app.none
backgroundtextnone, push (web push) or poll (a 15-minute check for new items).none
directory_idtextWebsite packs only: “my app is this id in Pulsar’s directory”. See Publish.none

Ratings, installs, size, category, tags and “Verified by Pulsar” are not in the pack: the directory keeps those.

Domains

  • Plain host names only: example.com, shop.example.com. No *, no https://, no path, no port, no IP address, no localhost.
  • Each domain covers itself and every subdomain: example.com also covers www.example.com and m.example.com.
  • A leading www. is dropped, and a domain covered by another is folded (www.example.com next to example.com is the same).
  • The domain must have a registrable name: a bare suffix like co.uk is refused.
  • Everything the pack points to — start_url, an icon image, share-target addresses — must be https on these domains. An icon image may also come from Pulsar’s own servers.

The strict checks

Pulsar checks every pack before it installs or updates it, and the directory runs the same rules before it publishes one, so a pack the directory accepts is never refused by a phone.

CheckRule
JSONA JSON object, plain JSON (no comments), at most 1 MB.
Requiredpulsar_app is 1; id, version, name and domains are there and well formed.
Addressesstart_url, icon images and share-target addresses are https on the app’s own domains, default port, no user name.
Colourstile_color, theme_color and swatches are #RRGGBB.
Lengthsname ≤ 40, short_name ≤ 12, changes titles ≤ 40, share-target names ≤ 40.
SettingsPass the same validator as extensions’ settings: known item types, unique ids, values that fit their options or range.
whenEvery when names a setting of this pack, with a value that setting can take.
CodeCSS and JS together ≤ 512 KB, at most 40 blocks; run_at, hosts and paths well formed.
HeadersRequest headers only; never credentials, routing or Sec- / Proxy- headers; Referer only removed.
Share targetsA link target needs {url} (or {text}), a text target needs {text}; {title} alone is refused. Some video sites can’t be share-target sites (store rules).
Check before you uploadThe App starter’s node build.mjs runs these checks on your computer and names the field that is wrong, like share_targets[0].url must be https on the app’s own domains.

A full example

The App starter’s pack, with its CSS and JS shortened. The real one is built from pack.src.json, app.css and app.js.

pack.json
{
  "pulsar_app": 1,
  "id": "example-kitchen",
  "version": "1.0.0",
  "name": "Example Kitchen",
  "short_name": "Kitchen",
  "tagline": "Recipes in their own window, with cook mode",
  "description": "Example Kitchen as an app: its own window and icon, its own login, a dark theme …",
  "developer": { "name": "Example Kitchen", "url": "https://example.com" },
  "start_url": "https://example.com/",
  "domains": ["example.com"],
  "icon": "star",
  "tile_color": "#E8590C",
  "theme_color": "#1F2937",
  "display": "standalone",
  "opens_as": "app",
  "changes": [
    { "title": "Dark theme", "kind": "added", "icon": "moon" },
    { "title": "Newsletter pop-up", "kind": "removed", "icon": "eye_off" },
    { "title": "Mobile layout", "kind": "improved", "icon": "globe" }
  ],
  "settings": { "sections": [
    { "title": "Look", "items": [
      { "type": "choice", "id": "theme", "title": "Theme", "value": "auto", "options": [
        { "value": "auto", "label": "Like the phone" },
        { "value": "light", "label": "Light" }, { "value": "dark", "label": "Dark" } ] },
      { "type": "chips", "id": "text_size", "value": "m", "options": [
        { "value": "s", "label": "S" }, { "value": "m", "label": "M" }, { "value": "l", "label": "L" } ] }
    ] },
    { "title": "Cooking", "items": [
      { "type": "toggle", "id": "cook_mode", "title": "Keep the screen on", "value": true },
      { "type": "toggle", "id": "hide_popup", "title": "Hide the newsletter pop-up", "value": true }
    ] }
  ] },
  "css": [
    { "code": ".get-app-banner { display: none !important; }", "run_at": "document_start" },
    { "code": ":root { color-scheme: dark; --site-bg: #121212; }", "when": { "theme": "dark" } },
    { "code": "html { font-size: 112.5% !important; }", "when": { "text_size": "l" } },
    { "code": ".newsletter-popup { display: none !important; }", "when": { "hide_popup": true } }
  ],
  "js": [
    { "code": "document.documentElement.classList.add('in-pulsar-app');", "run_at": "document_start" },
    { "code": "/* cook mode: navigator.wakeLock … */", "run_at": "document_end", "paths": ["/recipes/*"] }
  ],
  "headers": [
    { "action": "set", "name": "X-Example-Client", "value": "pulsar-app" },
    { "action": "remove", "name": "Referer", "paths": ["/out/*"] }
  ],
  "share_targets": [
    { "id": "save", "name": "Save as a recipe",
      "url": "https://example.com/save?url={url}&title={title}", "accepts": { "types": ["url"] } },
    { "id": "search", "name": "Search recipes",
      "url": "https://example.com/search?q={text}", "accepts": { "types": ["text"] } }
  ],
  "background": "none"
}