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 ofredirectUrls) 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,codeorworkflow. 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.0is 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.
{
"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:
| Group | Fields |
|---|---|
| Identity | schemaVersion, apiVersion, handle, releaseVersion, displayName, description, translations, appId |
| Links and your server | supportUrl, privacyUrl, homepageUrl, backendOrigin, redirectUrls |
| Store access | permissions |
| Admin and storefront UI | workers, elements, adminPages, storefrontPages, adminFrame |
| Server calls and events | actions, webhooks, destinations |
| App-level settings | settings |
| Advanced | extensions, uiRuntime, sources |
Identity
| Field | Type and limits | Required | Default |
|---|---|---|---|
schemaVersion | The number 1 | Yes | None |
apiVersion | The string "v1" | Yes | None |
handle | 2 to 64 characters: lowercase letters and digits joined by single hyphens | Yes | None |
releaseVersion | A semantic version without build metadata, such as 1.0.0 or 1.1.0-beta.1 | Yes | None |
displayName | 1 to 80 characters | Yes | None |
description | 1 to 1024 characters | Yes | None |
translations | A list of up to 16 entries, one per language | No | None |
translations[].locale | A language tag such as hu-HU or de | Yes | None |
translations[].displayName | 1 to 80 characters | No | None |
translations[].description | 1 to 1024 characters | No | None |
appId | app_ followed by 26 letters and digits | No | None |
schemaVersionandapiVersionname the manifest format and the App API version your app is written for. They are the only accepted values today.handleis 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.releaseVersionlabels this release for reviewers and merchants. Use a new version for every release you publish so you can tell them apart.displayNameanddescriptionare what merchants see on the permission screen and in their list of installed apps.translationsgive the name and description in other languages. Each entry needsdisplayName,descriptionor both, and eachlocalemay appear once.appIdis read only by the CLI:poltura app publishuses 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 withhandle must be lowercase letters and digits joined by single hyphens."releaseVersion": "v1.0.0"fails withmust be a semver release without build metadata. So does1.0.0+build.5.- A translation with only a
localefails:translation must include displayName or description.
Links and your server
| Field | Type and limits | Required | Default |
|---|---|---|---|
supportUrl | An HTTPS URL, at most 2048 characters | Yes | None |
privacyUrl | An HTTPS URL, at most 2048 characters | Yes | None |
homepageUrl | An HTTPS URL, at most 2048 characters | No | None |
backendOrigin | An HTTPS origin: scheme and host, optionally a port, nothing after it | Yes | None |
redirectUrls | 1 to 10 different HTTPS URLs | Yes | None |
supportUrlandprivacyUrlare shown to reviewers and merchants. The privacy page should say what store data you keep and for how long.backendOriginis your server. Actions are sent to it (seeactions).redirectUrlsare the addresses Poltura may send a merchant back to at the end of the connection. TheredirectUriyour 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 withHTTPS URL must be the exact canonical href; writehttps://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 ashttps://*.example.com/callback."backendOrigin": "https://parcel-helper.example.com/api"fails withbackend origin must not include a path or query.
Store access
| Field | Type and limits | Required | Default |
|---|---|---|---|
permissions | An object with two lists | Yes | None |
permissions.required | Up to 32 permission requests, together with optional | Yes | None |
permissions.required[].id | One of the permission ids, such as products.read | Yes | None |
permissions.required[].purpose | 8 to 280 characters | Yes | None |
permissions.optional | Up to 32 permission requests, together with required | Yes | None |
permissions.optional[].id | One of the permission ids | Yes | None |
permissions.optional[].purpose | 8 to 280 characters | Yes | None |
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.readnext to a requiredproducts.writefails withpermission "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:readwritten with a colon instead ofproducts.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.
| Field | Type and limits | Required | Default |
|---|---|---|---|
workers | 1 to 16 workers | No (see below) | None |
workers[].target | One of the UI targets, such as admin.order | Yes | None |
workers[].entry | Path of the .tsx file that calls startUiWorker, relative to the project | Yes | None |
workers[].stylesheet | Path of a CSS file, relative to the project | No | None |
workers[].inheritTheme | true or false | No | true |
workers[].element | The type of a defineBlock in your elements files | For storefront block targets | None |
workers[].path | Where the bundle goes in the release: a relative path ending in .js | No | workers/<entry file name>.js |
- A project needs
workers, or the advanceduiRuntimeandsourcespair instead. Every app has at least one worker; a hosted-only app still ships a small one, such as a home card. targetdecides 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.stylesheetis compiled with Tailwind and shipped with the release. WithinheritThemeleft 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.elementconnects a storefront block worker to thedefineBlockdefinition 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 forstorefront.app-pageand admin targets.
elements
| Field | Type and limits | Required | Default |
|---|---|---|---|
elements | One file path, or a list of 1 to 32 paths | When any worker or extension names an element | None |
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
| Field | Type and limits | Required | Default |
|---|---|---|---|
adminPages | Up to 8 pages | No | None |
adminPages[].id | 1 to 48 characters, snake_case | Yes | None |
adminPages[].path | The path of a worker whose target is admin.page | Yes | None |
adminPages[].title | Plain text, at most 60 characters | Yes | None |
adminPages[].translations | Up to 16 entries, one per language | No | None |
adminPages[].translations[].locale | A language tag such as hu-HU | Yes | None |
adminPages[].translations[].title | Plain text, at most 60 characters | Yes | None |
adminPages[].icon | One of the page icons listed below | No | None |
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
| Field | Type and limits | Required | Default |
|---|---|---|---|
storefrontPages | Up to 8 pages | No | None |
storefrontPages[].id | 1 to 48 characters, snake_case, different from every admin page id | Yes | None |
storefrontPages[].path | The path of a worker whose target is storefront.app-page | Yes | None |
storefrontPages[].title | Plain text, at most 60 characters | Yes | None |
storefrontPages[].slug | 1 to 48 characters: lowercase letters, digits and hyphens | Yes | None |
storefrontPages[].translations | Up to 16 entries, one per language | No | None |
storefrontPages[].translations[].locale | A language tag such as hu-HU | Yes | None |
storefrontPages[].translations[].title | Plain text, at most 60 characters | Yes | None |
storefrontPages[].icon | One of the page icons above | No | None |
storefrontPages[].seo | An object | No | { "indexable": false } |
storefrontPages[].seo.indexable | true or false | No | false |
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
elementfails withworker storefront.home is a block merchants place: name its defineBlock type in "element". elementwithoutelementsfails with"element": "parcel" needs "elements" pointing at the files with its defineBlock call.- A page
paththat is not a built worker path fails withadmin page "parcel" path "workers/parcel.js" must match a uiRuntime worker whose target is admin.page. The built path isworkers/plus the entry file's name with.js, unless the worker sets its ownpath. "slug": "cart"fails withslug "cart" is reserved.- Titles are plain text: HTML tags,
javascript:and similar are refused.
adminFrame
| Field | Type and limits | Required | Default |
|---|---|---|---|
adminFrame | An object | No | None |
adminFrame.url | An HTTPS URL with no query or fragment, on a public host name | Yes | None |
adminFrame.actions | A list of the bridge actions the page may use, each once | Yes | None |
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.
| Field | Type and limits | Required | Default |
|---|---|---|---|
actions | Up to 32 actions | No | None |
actions[].id | 1 to 48 characters, snake_case, starting with a letter | Yes | None |
actions[].path | A path on backendOrigin starting with /, at most 256 characters | No | /actions/<id> |
actions[].purpose | 8 to 280 characters | Yes | None |
actions[].surfaces | admin, storefront or both | Yes | None |
actions[].timeoutMs | 1000 to 10000 | No | 5000 |
actions[].maxInputBytes | 1 to 32768 | No | 32768 |
actions[].maxOutputBytes | 1 to 65536 | No | 65536 |
surfacessays where workers may call the action. A storefront call also carries the shopper.timeoutMsis how long the platform waits for your server before the worker's call fails.maxInputBytesandmaxOutputBytescap 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
| Field | Type and limits | Required | Default |
|---|---|---|---|
webhooks | Up to 16 subscriptions | No | None |
webhooks[].topic | One of the webhook topics, such as order.created | Yes | None |
webhooks[].destination | An HTTPS URL | Yes | None |
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
| Field | Type and limits | Required | Default |
|---|---|---|---|
destinations | Up to 16 | No | None |
destinations[].id | 1 to 64 characters, snake_case | Yes | None |
destinations[].url | An HTTPS URL | Yes | None |
destinations[].purpose | 8 to 280 characters | Yes | None |
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
| Field | Type and limits | Required | Default |
|---|---|---|---|
settings | Up to 32 fields | No | None |
settings[].id | 1 to 64 characters, snake_case | Yes | None |
settings[].type | string, number, boolean, url or enum | Yes | None |
settings[].label | 1 to 80 characters | Yes | None |
settings[].description | At most 280 characters | No | None |
settings[].required | true or false | No | None |
settings[].default | A value of the field's type: text up to 500 characters, an HTTPS URL, or one of options | No | None |
settings[].options | 1 to 32 values, each at most 64 characters | For enum | None |
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.
| Key | Type and limits |
|---|---|
identity_provider.spec.protocol | oidc or oauth2 |
identity_provider.spec.issuer | For oidc: an HTTPS URL |
identity_provider.spec.scopes | Up to 20 scope names; at least one for oidc |
identity_provider.spec.authorizeParams | Up to 8 extra query parameters for the authorization request, never one the sign-in sets itself |
identity_provider.spec.authorizationEndpoint | For oauth2: an HTTPS URL |
identity_provider.spec.tokenEndpoint | For oauth2: an HTTPS URL |
identity_provider.spec.userinfoEndpoint | For oauth2: an HTTPS URL |
identity_provider.spec.pkce | For oauth2: false only for a provider that refuses PKCE |
identity_provider.spec.claims | For oauth2: subject, and optionally email, emailVerified, givenName, familyName, name, username (the handle used when name is empty) |
identity_provider.settings.client_id | The id of the app setting that holds the merchant's client id |
identity_provider.settings.client_secret | The 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
SocialSignInpart draws the buttons; itsprovidersprop limits them by app id, such asauth-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.
| Field | Type and limits | Required | Default |
|---|---|---|---|
extensions | Up to 16 declarative JSON documents | No | None |
extensions[].kind | declarative_admin_page or declarative_storefront_block | Yes | None |
extensions[].target | admin.page for admin pages; storefront.home, storefront.product or storefront.cart for blocks | Yes | None |
extensions[].path | Path of the JSON document in the project | Yes | None |
extensions[].digest | The document's SHA-256 as sha256-<base64> | Yes | None |
extensions[].mediaType | application/vnd.poltura.extension.v1+json | Yes | None |
extensions[].element | The defineBlock type of a storefront block document | For declarative_storefront_block | None |
uiRuntime | An object, instead of workers | No | None |
uiRuntime.version | The number 1 | Yes | None |
uiRuntime.workers | 1 to 16 workers | Yes | None |
uiRuntime.workers[].target | One of the UI targets | Yes | None |
uiRuntime.workers[].path | Where the bundle goes in the release, ending in .js | Yes | None |
uiRuntime.workers[].stylesheet | An object | No | None |
uiRuntime.workers[].stylesheet.path | Where the CSS goes in the release | Yes | None |
uiRuntime.workers[].stylesheet.inheritTheme | true or false | No | true |
uiRuntime.workers[].element | The type of a defineBlock | For storefront block targets | None |
sources | A map from each uiRuntime path to the source file it is built from | With uiRuntime | None |
extensionsare declarative documents: JSON that describes a simple page or block, with no code. New apps use workers.uiRuntimewithsourcesnames the release paths yourself instead of lettingworkersderive 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.