Developers
Start building
Apps/Manifest

Manifest

Every field of poltura.app.json in plain words, grouped by purpose, with its type, limits, default, an example and the mistakes the CLI rejects.

On this page

poltura.app.json sits at the root of your app project and describes everything Poltura needs to know about the app: who it is, which store data it asks for, where its server lives, what it draws in the admin and on the storefront, and which events and actions it handles. poltura app build checks it, bundles your workers and writes the release manifest, dist/manifest.json. That file is what a reviewer approves and what a merchant installs.

The CLI is strict on purpose:

  • Unknown fields fail. A typo such as redirectUrl (instead of redirectUrls) is an error, not something quietly ignored.
  • Names that look like platform internals or credentials fail, wherever they appear: keys or ids such as token, secret, api_key, password, database, query, code or workflow. An app never receives platform access, so the manifest has no place to ask for it.
  • Errors name the field. The CLI prints the path and the reason, for example app config schema: permissions.required.0.purpose: purpose must explain the merchant-visible use. 0 is the first entry of the list.

A test checks this page against the CLI's schema: every table row, every required mark and default, and every JSON example below, which must be accepted as part of a real manifest. A field cannot be added or removed without this page changing too. Required means required inside its parent: actions[].id is required in every action, but actions itself is optional.

A small manifest

This is the complete manifest of the native widgets example, Parcel helper: two admin panels and two actions. Everything not listed takes its default. The shorter JSON examples further down are parts of a manifest like this one.

poltura.app.json
{
  "schemaVersion": 1,
  "apiVersion": "v1",
  "handle": "parcel-helper",
  "releaseVersion": "1.0.0",
  "displayName": "Parcel helper",
  "description": "Suggests a box size on order pages and checks product listings before they go live.",
  "translations": [
    {
      "locale": "hu-HU",
      "displayName": "Csomagsegéd",
      "description": "Dobozméretet javasol a rendeléseknél, és ellenőrzi a termékoldalakat élesítés előtt."
    }
  ],
  "supportUrl": "https://parcel-helper.example.com/support",
  "privacyUrl": "https://parcel-helper.example.com/privacy",
  "backendOrigin": "https://parcel-helper.example.com",
  "redirectUrls": ["https://parcel-helper.example.com/oauth/callback"],
  "permissions": {
    "required": [
      {
        "id": "orders.read",
        "purpose": "Read the items of the order you have open to suggest a box"
      },
      {
        "id": "products.read",
        "purpose": "Read the product you have open to check its listing"
      }
    ],
    "optional": []
  },
  "actions": [
    {
      "id": "suggest_box",
      "purpose": "Suggest a box size from the items of the open order",
      "surfaces": ["admin"]
    },
    {
      "id": "check_listing",
      "purpose": "Check the title, description and image of the open product",
      "surfaces": ["admin"],
      "timeoutMs": 3000
    }
  ],
  "workers": [
    {
      "target": "admin.order",
      "entry": "src/workers/order-panel.tsx",
      "stylesheet": "src/styles.css"
    },
    {
      "target": "admin.product",
      "entry": "src/workers/product-panel.tsx",
      "stylesheet": "src/styles.css"
    }
  ]
}

The fields, grouped by what they are for:

GroupFields
IdentityschemaVersion, apiVersion, handle, releaseVersion, displayName, description, translations, appId
Links and your serversupportUrl, privacyUrl, homepageUrl, backendOrigin, redirectUrls
Store accesspermissions
Admin and storefront UIworkers, elements, adminPages, storefrontPages, adminFrame
Server calls and eventsactions, webhooks, destinations
App-level settingssettings
Advancedextensions, uiRuntime, sources

Identity

FieldType and limitsRequiredDefault
schemaVersionThe number 1YesNone
apiVersionThe string "v1"YesNone
handle2 to 64 characters: lowercase letters and digits joined by single hyphensYesNone
releaseVersionA semantic version without build metadata, such as 1.0.0 or 1.1.0-beta.1YesNone
displayName1 to 80 charactersYesNone
description1 to 1024 charactersYesNone
translationsA list of up to 16 entries, one per languageNoNone
translations[].localeA language tag such as hu-HU or deYesNone
translations[].displayName1 to 80 charactersNoNone
translations[].description1 to 1024 charactersNoNone
appIdapp_ followed by 26 letters and digitsNoNone
  • schemaVersion and apiVersion name the manifest format and the App API version your app is written for. They are the only accepted values today.
  • handle is your app's permanent id. It must be exactly the Handle you chose when you created the app in Developer apps; a release with any other handle is refused when you publish. You cannot change it later.
  • releaseVersion labels this release for reviewers and merchants. Use a new version for every release you publish so you can tell them apart.
  • displayName and description are what merchants see on the permission screen and in their list of installed apps.
  • translations give the name and description in other languages. Each entry needs displayName, description or both, and each locale may appear once.
  • appId is read only by the CLI: poltura app publish uses it when you leave out --app. It is not copied into the release manifest.
{
  "handle": "parcel-helper",
  "releaseVersion": "1.2.0",
  "displayName": "Parcel helper",
  "description": "Suggests a box size on order pages.",
  "translations": [{ "locale": "hu-HU", "displayName": "Csomagsegéd" }],
  "appId": "0199f3a2-5b1c-7d4e-9f20-3a4b5c6d7e8f"
}

Common mistakes:

  • "handle": "Parcel-Helper" fails with handle must be lowercase letters and digits joined by single hyphens.
  • "releaseVersion": "v1.0.0" fails with must be a semver release without build metadata. So does 1.0.0+build.5.
  • A translation with only a locale fails: translation must include displayName or description.
FieldType and limitsRequiredDefault
supportUrlAn HTTPS URL, at most 2048 charactersYesNone
privacyUrlAn HTTPS URL, at most 2048 charactersYesNone
homepageUrlAn HTTPS URL, at most 2048 charactersNoNone
backendOriginAn HTTPS origin: scheme and host, optionally a port, nothing after itYesNone
redirectUrls1 to 10 different HTTPS URLsYesNone
  • supportUrl and privacyUrl are shown to reviewers and merchants. The privacy page should say what store data you keep and for how long.
  • backendOrigin is your server. Actions are sent to it (see actions).
  • redirectUrls are the addresses Poltura may send a merchant back to at the end of the connection. The redirectUri your server passes to the SDK's OAuth client must match one of them character for character. The list is required even if your app never calls the App API; point it at the callback route you would use. See Authentication.

Every URL must be written in its exact canonical form, the way a browser would print it. That catches a few things people do not expect:

  • An origin on its own needs the trailing slash: "homepageUrl": "https://parcel-helper.example.com" fails with HTTPS URL must be the exact canonical href; write https://parcel-helper.example.com/.
  • Host names are lowercase, default ports are left out, and there is no #fragment, user name or password.
  • http:// fails, as do wildcards such as https://*.example.com/callback.
  • "backendOrigin": "https://parcel-helper.example.com/api" fails with backend origin must not include a path or query.

Store access

FieldType and limitsRequiredDefault
permissionsAn object with two listsYesNone
permissions.requiredUp to 32 permission requests, together with optionalYesNone
permissions.required[].idOne of the permission ids, such as products.readYesNone
permissions.required[].purpose8 to 280 charactersYesNone
permissions.optionalUp to 32 permission requests, together with requiredYesNone
permissions.optional[].idOne of the permission idsYesNone
permissions.optional[].purpose8 to 280 charactersYesNone

A permission lets your app read or change one kind of store data through the App API. They are the same permissions merchants give their team members. The merchant sees every permission with your purpose on the install screen before they approve, and grants all of the required ones. They may turn down optional permissions, so check what was granted (see Authentication) before you call an API that needs one. An app that reads no store data may ask for none.

Write the purpose for the merchant, about their store: "Read the items of the order you have open to suggest a box", not "Needed by the API".

"permissions": {
  "required": [{ "id": "products.read", "purpose": "List your products to pick featured ones" }],
  "optional": [{ "id": "products.write", "purpose": "Mark your featured products in the catalog" }]
}

Common mistakes:

  • Leaving out optional: both lists are required, so write "optional": [].
  • The same permission in both lists, or twice in one list, fails with permission "orders.read" cannot be both required and optional.
  • A required write takes its read with it, so the read cannot be optional: products.read next to a required products.write fails with permission "products.read" comes with the required "products.write" and cannot be optional.
  • A purpose shorter than 8 characters fails with purpose must explain the merchant-visible use.
  • An id that is not in the catalogue, such as products:read written with a colon instead of products.read.
  • Asking for more than the app uses. The reviewer and the merchant see every permission; ask only for what the app needs.

Admin and storefront UI

workers

A worker is one piece of native UI, written in React with Poltura's components and bundled by the CLI. Each worker renders into one target: a place in the admin or on the storefront.

FieldType and limitsRequiredDefault
workers1 to 16 workersNo (see below)None
workers[].targetOne of the UI targets, such as admin.orderYesNone
workers[].entryPath of the .tsx file that calls startUiWorker, relative to the projectYesNone
workers[].stylesheetPath of a CSS file, relative to the projectNoNone
workers[].inheritThemetrue or falseNotrue
workers[].elementThe type of a defineBlock in your elements filesFor storefront block targetsNone
workers[].pathWhere the bundle goes in the release: a relative path ending in .jsNoworkers/<entry file name>.js
  • A project needs workers, or the advanced uiRuntime and sources pair instead. Every app has at least one worker; a hosted-only app still ships a small one, such as a home card.
  • target decides where the worker appears. Admin targets are pages, the home and the detail panels of products, collections, orders and customers. Storefront targets are blocks merchants place on a template, the header and footer zones, and app pages. See Admin and Storefront.
  • stylesheet is compiled with Tailwind and shipped with the release. With inheritTheme left on, the host's colours, fonts, radii, shadows and spacing are passed into the worker's styles, so it matches the admin or the store's theme. See Styling.
  • element connects a storefront block worker to the defineBlock definition that gives it a name, an icon and settings in the Customizer. It is required for the storefront template targets and the header and footer zones, and optional for storefront.app-page and admin targets.

elements

FieldType and limitsRequiredDefault
elementsOne file path, or a list of 1 to 32 pathsWhen any worker or extension names an elementNone

The files that hold your defineBlock calls. The build reads them without running them and stops with the file, line and column of any definition it cannot read. See Settings and translations.

adminPages

FieldType and limitsRequiredDefault
adminPagesUp to 8 pagesNoNone
adminPages[].id1 to 48 characters, snake_caseYesNone
adminPages[].pathThe path of a worker whose target is admin.pageYesNone
adminPages[].titlePlain text, at most 60 charactersYesNone
adminPages[].translationsUp to 16 entries, one per languageNoNone
adminPages[].translations[].localeA language tag such as hu-HUYesNone
adminPages[].translations[].titlePlain text, at most 60 charactersYesNone
adminPages[].iconOne of the page icons listed belowNoNone

An admin.page worker on its own is not reachable. Listing it here adds it to the Apps group of the admin navigation, at /apps/<installation id>/<page id>. The admin shows the title of the translation whose locale matches the merchant's language exactly, then one with the same language (hu for hu-HU), then title.

Page icons: home, box, boxes, cart, receipt, users, user, tag, settings, chart, globe, mail, file, files, image, link, star, heart, grid, list, search, bell, calendar, message.

storefrontPages

FieldType and limitsRequiredDefault
storefrontPagesUp to 8 pagesNoNone
storefrontPages[].id1 to 48 characters, snake_case, different from every admin page idYesNone
storefrontPages[].pathThe path of a worker whose target is storefront.app-pageYesNone
storefrontPages[].titlePlain text, at most 60 charactersYesNone
storefrontPages[].slug1 to 48 characters: lowercase letters, digits and hyphensYesNone
storefrontPages[].translationsUp to 16 entries, one per languageNoNone
storefrontPages[].translations[].localeA language tag such as hu-HUYesNone
storefrontPages[].translations[].titlePlain text, at most 60 charactersYesNone
storefrontPages[].iconOne of the page icons aboveNoNone
storefrontPages[].seoAn objectNo{ "indexable": false }
storefrontPages[].seo.indexabletrue or falseNofalse

A storefront page is served at /apps/<slug> on the merchant's store. Leave indexable off unless the page has content search engines should list. These slugs are taken by the store and cannot be used: products, cart, checkout, account, profile, blog, guides, legal, contact, faq, claim, review, order, auth, pages, apps, api, sitemap, robots.

"workers": [
  { "target": "admin.page", "entry": "src/workers/settings-page.tsx" },
  { "target": "storefront.app-page", "entry": "src/workers/wishlist.tsx", "element": "wishlist-page" }
],
"elements": "src/elements.ts",
"adminPages": [{ "id": "settings", "path": "workers/settings-page.js", "title": "Settings", "icon": "settings" }],
"storefrontPages": [
  {
    "id": "wishlist",
    "path": "workers/wishlist.js",
    "title": "Wishlist",
    "slug": "wishlist",
    "translations": [{ "locale": "hu-HU", "title": "Kívánságlista" }]
  }
]

Common mistakes:

  • A storefront block worker without element fails with worker storefront.home is a block merchants place: name its defineBlock type in "element".
  • element without elements fails with "element": "parcel" needs "elements" pointing at the files with its defineBlock call.
  • A page path that is not a built worker path fails with admin page "parcel" path "workers/parcel.js" must match a uiRuntime worker whose target is admin.page. The built path is workers/ plus the entry file's name with .js, unless the worker sets its own path.
  • "slug": "cart" fails with slug "cart" is reserved.
  • Titles are plain text: HTML tags, javascript: and similar are refused.

adminFrame

FieldType and limitsRequiredDefault
adminFrameAn objectNoNone
adminFrame.urlAn HTTPS URL with no query or fragment, on a public host nameYesNone
adminFrame.actionsA list of the bridge actions the page may use, each onceYesNone

A hosted page is a page you serve from your own server, shown inside the admin in an iframe. It is the choice when you need your own HTML and JavaScript. actions lists what the page may ask the admin to do: toast, set_loading, navigate_host, pick_resource, resize, get_session_token and get_appearance. Add get_session_token if the page calls your server; see Authentication.

"adminFrame": { "url": "https://catalog-glance.example.com/app", "actions": ["get_session_token"] }

localhost, *.localhost and IP addresses fail with App pages require a public HTTPS hostname. Try hosted pages on a real HTTPS host name, such as a tunnel.

Server calls and events

actions

An action is a call from a native worker to your server. The worker calls useAction('suggest_box'), the platform signs the request and sends it to your server, and your server's answer comes back to the worker. Your server never receives the merchant's credentials, and the worker never receives yours.

FieldType and limitsRequiredDefault
actionsUp to 32 actionsNoNone
actions[].id1 to 48 characters, snake_case, starting with a letterYesNone
actions[].pathA path on backendOrigin starting with /, at most 256 charactersNo/actions/<id>
actions[].purpose8 to 280 charactersYesNone
actions[].surfacesadmin, storefront or bothYesNone
actions[].timeoutMs1000 to 10000No5000
actions[].maxInputBytes1 to 32768No32768
actions[].maxOutputBytes1 to 65536No65536
  • surfaces says where workers may call the action. A storefront call also carries the shopper.
  • timeoutMs is how long the platform waits for your server before the worker's call fails.
  • maxInputBytes and maxOutputBytes cap the JSON going each way; lower them to what your action needs.

How to receive and verify actions on your server, with createActionHandler, is on Authentication.

Common mistakes: "id": "suggestBox" fails with action id must be snake_case; "timeoutMs": 20000 fails with Number must be less than or equal to 10000; "id": "token" fails with action id "token" is not allowed; a path with ?, #, .. or // fails with action path must be relative to backendOrigin.

webhooks

FieldType and limitsRequiredDefault
webhooksUp to 16 subscriptionsNoNone
webhooks[].topicOne of the webhook topics, such as order.createdYesNone
webhooks[].destinationAn HTTPS URLYesNone

Poltura sends a signed POST to destination when the event happens in a store that installed your app. Each topic needs the permission that reads its data: order.* needs orders.read. The app.* lifecycle topics need none. Each topic and each destination may appear once. See Webhooks.

destinations

FieldType and limitsRequiredDefault
destinationsUp to 16NoNone
destinations[].id1 to 64 characters, snake_caseYesNone
destinations[].urlAn HTTPS URLYesNone
destinations[].purpose8 to 280 charactersYesNone

Other addresses on your side that receive store data, declared so they are part of the reviewed release. Poltura checks and stores them but sends nothing to them today. Ids and URLs must be unique.

App-level settings

FieldType and limitsRequiredDefault
settingsUp to 32 fieldsNoNone
settings[].id1 to 64 characters, snake_caseYesNone
settings[].typestring, number, boolean, url or enumYesNone
settings[].label1 to 80 charactersYesNone
settings[].descriptionAt most 280 charactersNoNone
settings[].requiredtrue or falseNoNone
settings[].defaultA value of the field's type: text up to 500 characters, an HTTPS URL, or one of optionsNoNone
settings[].options1 to 32 values, each at most 64 charactersFor enumNone

Poltura checks these fields and keeps them with the release, but no admin screen shows them yet. Settings a merchant edits today belong to a worker: they come from its defineBlock definition and are read with useSettings(). See Settings and translations.

Common mistake: an enum default that is not in options fails with enum default must be one of the options.

Sign-in providers

A sign-in app lets shoppers sign in to a store with an outside account such as Google or Discord. Poltura's own sign-in apps declare their provider in their manifest as identity_provider; the poltura.app.json format on this page does not accept it yet, so a published app cannot add one.

KeyType and limits
identity_provider.spec.protocoloidc or oauth2
identity_provider.spec.issuerFor oidc: an HTTPS URL
identity_provider.spec.scopesUp to 20 scope names; at least one for oidc
identity_provider.spec.authorizeParamsUp to 8 extra query parameters for the authorization request, never one the sign-in sets itself
identity_provider.spec.authorizationEndpointFor oauth2: an HTTPS URL
identity_provider.spec.tokenEndpointFor oauth2: an HTTPS URL
identity_provider.spec.userinfoEndpointFor oauth2: an HTTPS URL
identity_provider.spec.pkceFor oauth2: false only for a provider that refuses PKCE
identity_provider.spec.claimsFor oauth2: subject, and optionally email, emailVerified, givenName, familyName, name, username (the handle used when name is empty)
identity_provider.settings.client_idThe id of the app setting that holds the merchant's client id
identity_provider.settings.client_secretThe id of the app setting that holds the merchant's client secret
  • The merchant brings their own credentials. They create a client with the provider and enter its id and secret in the app's settings. The secret is never sent to the shopper's browser.
  • One redirect address. The app's setup steps show the single redirect address the merchant registers with the provider.
  • One button per app. While the app is installed, enabled and both credentials are set, the storefront shows a sign-in button for it once the registration is confirmed, which takes up to a minute after a change. Switching the app off, uninstalling it or clearing a credential removes the button.
  • Themes. The SocialSignIn part draws the buttons; its providers prop limits them by app id, such as auth-google.
  • The shopper's address. A shopper whose provider vouches for the address signs in directly. If an account with that address exists, or the provider does not vouch for the address, a code is emailed to that address first. A provider that returns no address makes the storefront ask for one and prove it the same way.

Advanced

Most apps never need these.

FieldType and limitsRequiredDefault
extensionsUp to 16 declarative JSON documentsNoNone
extensions[].kinddeclarative_admin_page or declarative_storefront_blockYesNone
extensions[].targetadmin.page for admin pages; storefront.home, storefront.product or storefront.cart for blocksYesNone
extensions[].pathPath of the JSON document in the projectYesNone
extensions[].digestThe document's SHA-256 as sha256-<base64>YesNone
extensions[].mediaTypeapplication/vnd.poltura.extension.v1+jsonYesNone
extensions[].elementThe defineBlock type of a storefront block documentFor declarative_storefront_blockNone
uiRuntimeAn object, instead of workersNoNone
uiRuntime.versionThe number 1YesNone
uiRuntime.workers1 to 16 workersYesNone
uiRuntime.workers[].targetOne of the UI targetsYesNone
uiRuntime.workers[].pathWhere the bundle goes in the release, ending in .jsYesNone
uiRuntime.workers[].stylesheetAn objectNoNone
uiRuntime.workers[].stylesheet.pathWhere the CSS goes in the releaseYesNone
uiRuntime.workers[].stylesheet.inheritThemetrue or falseNotrue
uiRuntime.workers[].elementThe type of a defineBlockFor storefront block targetsNone
sourcesA map from each uiRuntime path to the source file it is built fromWith uiRuntimeNone
  • extensions are declarative documents: JSON that describes a simple page or block, with no code. New apps use workers.
  • uiRuntime with sources names the release paths yourself instead of letting workers derive them:
"uiRuntime": {
  "version": 1,
  "workers": [{ "target": "storefront.home", "path": "workers/home.js", "element": "home-offer" }]
},
"sources": { "workers/home.js": "src/workers/storefront-home.tsx" },
"elements": "src/elements.ts"

Release limits

  • 16 workers per release, each bundle at most 1 MiB, each stylesheet at most 256 KiB.
  • The release manifest is at most 256 KiB, and a whole release upload at most 22 MiB.
  • One release of an app can be in review at a time.

A complete example

The manifest of the example app walkthrough, with five workers, actions, webhooks and pages:

{
  "schemaVersion": 1,
  "apiVersion": "v1",
  "handle": "recommendations",
  "releaseVersion": "1.0.0",
  "displayName": "Recommendations by Poltura Labs",
  "description": "Product recommendations for admin settings, product panels, storefront blocks, footer capture, and an app-owned page.",
  "translations": [
    {
      "locale": "hu-HU",
      "displayName": "Ajánlások a Poltura Labs-tól",
      "description": "Termékajánlók az adminhoz, a termékoldalhoz, a lábléchez és egy alkalmazásoldalhoz."
    }
  ],
  "supportUrl": "https://recommendations.apps.example.com/support",
  "privacyUrl": "https://recommendations.apps.example.com/privacy",
  "homepageUrl": "https://recommendations.apps.example.com/",
  "backendOrigin": "https://recommendations.apps.example.com",
  "redirectUrls": ["https://recommendations.apps.example.com/oauth/callback"],
  "permissions": {
    "required": [
      {
        "id": "settings.read",
        "purpose": "Read store locale, currency, and public capabilities"
      },
      {
        "id": "products.read",
        "purpose": "List products and collections used as recommendation sources"
      },
      {
        "id": "orders.read",
        "purpose": "Read new orders that refresh recommendation weights"
      }
    ],
    "optional": []
  },
  "webhooks": [
    {
      "topic": "order.created",
      "destination": "https://recommendations.apps.example.com/webhooks/orders"
    },
    {
      "topic": "product.updated",
      "destination": "https://recommendations.apps.example.com/webhooks/products"
    }
  ],
  "settings": [
    {
      "id": "highlight_mode",
      "type": "enum",
      "label": "Highlight mode",
      "options": ["quiet", "featured"],
      "default": "quiet"
    }
  ],
  "actions": [
    {
      "id": "save_featured",
      "purpose": "Save featured product ids for this installation",
      "surfaces": ["admin"]
    },
    {
      "id": "recommend",
      "purpose": "Return a product recommendation for the current shopper",
      "surfaces": ["storefront"]
    }
  ],
  "destinations": [
    {
      "id": "newsletter_capture",
      "url": "https://recommendations.apps.example.com/capture",
      "purpose": "Receive optional newsletter captures from the footer worker"
    }
  ],
  "adminPages": [
    {
      "id": "recommendations",
      "path": "workers/admin-page.js",
      "title": "Recommendations",
      "translations": [
        {
          "locale": "hu-HU",
          "title": "Ajánlások"
        }
      ],
      "icon": "star"
    }
  ],
  "storefrontPages": [
    {
      "id": "recs_page",
      "path": "workers/app-page.js",
      "title": "Recommendations",
      "slug": "recommendations",
      "translations": [
        {
          "locale": "hu-HU",
          "title": "Ajánlások"
        }
      ],
      "icon": "star"
    }
  ],
  "elements": "src/elements.ts",
  "workers": [
    {
      "target": "admin.page",
      "entry": "src/workers/admin-page.tsx",
      "stylesheet": "src/styles.css",
      "element": "recommendations-panel"
    },
    {
      "target": "admin.product",
      "entry": "src/workers/admin-product.tsx",
      "stylesheet": "src/styles.css"
    },
    {
      "target": "storefront.product",
      "entry": "src/workers/storefront-product.tsx",
      "stylesheet": "src/styles.css",
      "element": "recommendations"
    },
    {
      "target": "storefront.layout.footer",
      "entry": "src/workers/storefront-footer.tsx",
      "stylesheet": "src/styles.css",
      "element": "newsletter-capture"
    },
    {
      "target": "storefront.app-page",
      "entry": "src/workers/app-page.tsx",
      "stylesheet": "src/styles.css",
      "element": "recommendations-page"
    }
  ]
}

Next

  • Authentication: OAuth, session tokens and signed actions, which several fields above refer to.
  • UI: what to put in a worker.
Last updated