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": 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
| Field | Type | Limits | Default |
|---|---|---|---|
| pulsar_app | number | Must be 1 (the format version). Required. | — |
| id | text | 2–63 of a–z 0–9 . _ -, lower case. Required. Never change it: it is how updates find the app. | — |
| version | text | Numbers and dots, 1–4 parts (1.4.0). Required. Every change is a higher version. | — |
| name | text | Up to 40 characters. Required. | — |
| short_name | text | Home-screen label, up to 12 characters. | name cut to 12 |
| tagline | text | Up to 80 characters, shown in lists. | none |
| description | text | Up to 600 characters, shown on the detail page. | none |
| developer | text or object | "Your name" or {"name", "url"}; url must be https. | none |
| start_url | text | https, on the app’s domains. http is refused. | https://<first domain>/ |
| domains | list of text | 1–10 plain domains. Each covers itself and all its subdomains. Required. | — |
| icon | text or object | A Pulsar icon name (white on tile_color), an https image on your domains, or {"glyph", "url"}. | a plain tile |
| tile_color | text | #RRGGBB, the icon tile. | chosen by Pulsar |
| theme_color | text | #RRGGBB, App mode’s top bar. | tile_color |
| display | text | standalone (slim top bar) or fullscreen (no bar). | standalone |
| opens_as | text | app (App mode) or tab (a browser tab). People can change it. | app |
| plugins | list of text | Up 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 |
| changes | list | Up to 8 lines for “What Pulsar changes”: {"title" (≤ 40), "kind": "added" | "removed" | "improved", "icon"?}. | none |
| settings | object | A SettingSpec, {"sections": [...]}: at most 12 sections, 40 items. See Settings. | none |
| css | list | CSS blocks. See CSS, JS and headers. | none |
| js | list | JavaScript blocks, same shape as css. | none |
| headers | list | Up to 20 request-header changes on your own pages. | none |
| share_targets | list | Up to 5 places a shared link or text can open in the app. | none |
| background | text | none, push (web push) or poll (a 15-minute check for new items). | none |
| directory_id | text | Website 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*, nohttps://, no path, no port, no IP address, nolocalhost. - Each domain covers itself and every subdomain:
example.comalso coverswww.example.comandm.example.com. - A leading
www.is dropped, and a domain covered by another is folded (www.example.comnext toexample.comis the same). - The domain must have a registrable name: a bare suffix like
co.ukis 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.
| Check | Rule |
|---|---|
| JSON | A JSON object, plain JSON (no comments), at most 1 MB. |
| Required | pulsar_app is 1; id, version, name and domains are there and well formed. |
| Addresses | start_url, icon images and share-target addresses are https on the app’s own domains, default port, no user name. |
| Colours | tile_color, theme_color and swatches are #RRGGBB. |
| Lengths | name ≤ 40, short_name ≤ 12, changes titles ≤ 40, share-target names ≤ 40. |
| Settings | Pass the same validator as extensions’ settings: known item types, unique ids, values that fit their options or range. |
when | Every when names a setting of this pack, with a value that setting can take. |
| Code | CSS and JS together ≤ 512 KB, at most 40 blocks; run_at, hosts and paths well formed. |
| Headers | Request headers only; never credentials, routing or Sec- / Proxy- headers; Referer only removed. |
| Share targets | A 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.
{
"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"
}