Storeware Docs

Hosting on Storeware

Run your app on Storeware's edge, free, at its own address, with no server of your own. In beta.

Early access

The CLI and the SDK are not on npm yet, so the commands below do not run for everyone. If you would like to build an app now, tell us.

In beta. Hosting is free while it is in beta. The limits below may change before it leaves beta, and we will say so in the changelog first.

An app does not need a server of its own. Storeware can run it for you, on its edge, close to whoever calls it. Your app lives at its own address:

https://<your slug>.storewareapps.com

Every call Storeware makes to an app reaches a hosted app exactly as it reaches one on your own server: webhooks, privacy requests, your home in the admin with its ?session=, storefront blocks and sections, the app proxy, delivery rates, automation actions and tools. Each is signed the same way, and your code verifies it with the same SDK.

Start one

npx @storeware/cli init --template hosted

This makes a folder with storeware.app.toml, src/worker.ts and a package.json. Every address in the manifest is already on your app's own name. To send it:

npx @storeware/cli app deploy

Deploy bundles src/worker.ts into one file, uploads it, and sends the manifest as a new release. A change of code is a new release, checked like any other: shops that have your app take it straight away when it reaches no further than before, and are asked when it does.

[runtime]

A [runtime] table in the manifest is what makes an app hosted.

storeware.app.toml
[app]
slug = "sizing"
url = "https://sizing.storewareapps.com/"

[webhooks]
url = "https://sizing.storewareapps.com/webhooks"
events = ["product.created.v1"]

[runtime]
main = "src/worker.ts"
egress = ["api.sizes.example"]
KeyMeaning
mainThe file deploy bundles. src/worker.ts when not given.
egressHosts your code may call besides Storeware's API, at most 20. Each is shown to the merchant who installs the app.
bundleWritten by deploy into the copy it sends. Never write it yourself.

Only your own address may appear on storewareapps.com, and only in an app with [runtime].

Your code

src/worker.ts exports a fetch(request, env) handler, like a Cloudflare Worker. Build it with createHostedApp, which gives you the same object as createStorewareApp:

src/worker.ts
import { createHostedApp, type HostedEnv } from '@storeware/plugin-sdk';

export default {
  async fetch(request: Request, env: HostedEnv): Promise<Response> {
    const storeware = createHostedApp(env);
    storeware.webhooks.on('product.created.v1', async (event) => {
      const client = await storeware.clientFor(event.storeId!);
      // Call the API for this shop.
    });

    const url = new URL(request.url);
    if (url.pathname === '/webhooks') return storeware.webhooks.handle(request);
    if (url.pathname === '/') {
      const { claims } = await storeware.authenticateAdmin(request);
      return new Response(`Hello, ${claims.store_id}`);
    }
    return new Response('Not found', { status: 404 });
  },
};

What is different from an app on your own server:

  • No client secret and no installation store. Storeware already holds every installation of your app and hands over the one a request is for.
  • Installs need no grant. app.installed.v1 still arrives, but there is nothing to trade.
  • Tokens are never refreshed. clientFor uses a live token from Storeware and asks for a new one when it is about to run out.

The same app can move between hosted and your own server by swapping createHostedApp(env) for createStorewareApp({ ... }), and back. The handlers you mount do not change.

env.STOREWARE

env holds four things and nothing else:

NameWhat it is
STOREWAREYour app's installations and its data. It only ever answers about your own app.
STOREWARE_API_URLhttps://api.storeware.ai
STOREWARE_APPYour slug.
STOREWARE_APP_ORIGINhttps://<your slug>.storewareapps.com

env.STOREWARE.installation({ storeId }) or ({ installationId }) returns an installation: its shop, its scopes, its status, its two secrets and a fresh access token. You rarely need it directly, since createHostedApp calls it for you.

Your code only sees the shops that run its own release. A shop that has not yet approved a new release keeps the older one, and the new code gets null for it, as if it were not installed. Requests that name no shop run the release merchants were last given. If a shop asks for too many tokens in one minute, the installation comes back with accessToken: null for a moment; its secrets are still there.

App data

Each installation has a small store of its own, for settings, a cursor, or what a shop chose. Use appData for typed reads and writes:

import { appData } from '@storeware/plugin-sdk';

const data = appData(env, installation.installationId);
await data.put('settings', { unit: 'cm' });
const settings = await data.get<{ unit: string }>('settings');
const page = await data.list({ prefix: 'size:' });

Values are kept as JSON. Per installation:

LimitValue
A key200 characters
A value, as JSON64 KB
Keys1,000
All values together5 MB
A page of list100 entries

A write past a limit throws, with a message that says which.

What your code may reach

Your code may call Storeware's API and the hosts in egress, over https, and nothing else:

  • Only GET, HEAD, POST, PUT, PATCH and DELETE.
  • Your headers are passed on, Authorization included, except cookies and headers that say where a request came from.
  • A redirect is handed back to your code, not followed. If you follow it, the new address is checked again.
  • A request body may be up to 1 MB, and an answer up to 5 MB.
  • No hosted app may call another, at any address on storewareapps.com.

Cookies your app sets are kept to its own address: Storeware drops any Domain from them, so no other app can read them.

A refused call answers 403 with the reason. Adding a host is a new release that each merchant approves.

Limits

Each call to your app may use:

LimitValue
CPU time100 ms
Calls out (fetch)50
Time to start answering30 seconds

A call past the CPU or fetch limit stops and answers 500. Waiting on the network does not count as CPU time.

Each installation also has a daily allowance of calls and CPU time, and so does your app for calls that name no installation. Past it, calls answer 429 with Retry-After until midnight UTC. Storeware's own signed calls and merchants signed in to your app in the admin are never refused for the allowance, so strangers cannot use it up and shut them out.

One address may call your app 600 times a minute. Past that, it gets 429 for the rest of the minute. Storeware's own signed calls are not counted.

Logs

npx @storeware/cli app logs --console --follow

Shows what your code logged with console.log, its errors, and each call that stopped early. A call keeps its first 50 lines, each up to 2,000 characters.

On this page