Skip to content

Starter template

embedded-node is a complete, runnable embedded app in plain node:http — no framework, no build step, one runtime dependency (@onebooks/node). It’s what onebooks init scaffolds and what the quickstart runs. It demonstrates every part of an embedded app at once: pages inside OneBooks, a session-token-authenticated backend, app data, a signed webhook receiver and an example hosted function.

Terminal window
onebooks init my-onebooks-app
cd my-onebooks-app
cp .env.example .env
npm install
node server.mjs

You need the onebooks CLI (in preview — see there for access). init points the app’s @onebooks/node dependency at the SDK version matching your CLI.

  • Home page (/) — the page merchants open from your app’s row in the Apps sidebar. Shows the business name, language and theme from app.context(), and the ten most recently created invoices.
  • Invoice action (/invoice-action, target INVOICE_ACTION) — opened from an invoice’s Apps menu. It saves a demo tracking number for that invoice as app data, shows a toast and closes itself.
  • Invoice block (/invoice-block, target INVOICE_BLOCK) — a card on the invoice page that reads the tracking number back, or shows Not set yet. OneBooks reloads it when the action dialog closes, so the new value appears straight away.
  • Webhook receiver (POST /webhooks) — verifies the signature, then drops the business’s stored tokens on app.uninstalled and business.redact.
  • Hosted function (functions/notify-on-paid.js) — deployed separately with the CLI; logs a line when an invoice is paid.
  • server.mjs the whole HTTP server — routes, API error handling, webhook receiver
  • Directorylib
    • load-env.mjs tiny .env loader, imported first
    • onebooks.mjs wires @onebooks/node to the token store — session-token verification, exchange and refresh
    • session-tokens.mjs the exchange-once / refresh / re-exchange rules, one renewal at a time, as pure logic
    • csp.mjs the Content-Security-Policy frame-ancestors value
    • pages.mjs the three HTML pages (home, invoice action, invoice block)
    • token-store.mjs a JSON-file token store, keyed by business and user
  • Directoryfunctions
    • notify-on-paid.js example hosted function (invoice.paid)
  • Directorystatic
    • app-bridge.global.js the App Bridge SDK’s script-tag build (plus its .map)
  • Directorytest
    • session-tokens.test.mjs
    • csp.test.mjs
  • Directorydata/ created at runtime — holds tokens.json, ignored by git
    • …
  • .env.example
  • .gitignore
  • package.json
  • README.md
  • LICENSE
RouteServes
GET /The home page
GET /invoice-actionThe INVOICE_ACTION extension page
GET /invoice-blockThe INVOICE_BLOCK extension page
GET /static/*app-bridge.global.js
GET /api/invoices/recentGET /invoices?limit=10, returned to the home page as { invoices, total }
POST /api/invoice-action?resourceId=…Reads the invoice, generates TRK-<invoice number>-…, saves it as the tracking_number app-data value, returns { trackingNumber }
GET /api/invoice-block?resourceId=…Reads the invoice’s app data and returns { trackingNumber } (or null)
POST /webhooksThe signed webhook receiver

Every /api/* route requires Authorization: Bearer <session token> — which app.fetch() attaches for you. Its errors are mapped to statuses your page can act on:

StatusMeans
400A bad request, such as a missing resourceId
401No valid session token — or OneBooks no longer accepts this session’s tokens and they couldn’t be renewed; reload the app
502OneBooks failed or refused: the token exchange didn’t work, or an API call came back with an error (its status and code are in the body)
500A bug in the app itself

npm test runs the tests for the token rules and the CSP header — no network needed.

VariableRequiredMeaning
ONEBOOKS_CLIENT_IDYesYour app’s client_id. The server refuses to start without it.
ONEBOOKS_CLIENT_SECRETYesYour app’s client_secret — the app must be CONFIDENTIAL, because token exchange requires a secret.
ONEBOOKS_WEBHOOK_SECRETFor webhooksThe whsec_… secret shown once when you create the webhook. Without it every delivery gets a 400.
ONEBOOKS_API_BASENoDefault https://api.getonebooks.com
PORTNoDefault 3100
APP_ORIGINNoThe OneBooks origin allowed to frame the pages. Default https://app.getonebooks.com
NODE_ENVIn productionSet it to production when you deploy: only APP_ORIGIN may frame the app then. Anything else also allows http://localhost:5173, a OneBooks frontend running locally.
Console tabSetting
OAuthScopes invoices:read, app-data:read, app-data:write
App dataA field on INVOICE with key tracking_number, field type TEXT, Merchant visible on
EmbeddingApp URL = your https origin; extensions INVOICE_ACTION → /invoice-action and INVOICE_BLOCK → /invoice-block
WebhooksOptional: an endpoint at <your origin>/webhooks subscribed to app.uninstalled and business.redact

The quickstart walks through each one.

Every page loads /static/app-bridge.global.js, calls app.ready(), and then reaches its backend with app.fetch('/api/…'). On the server:

  1. verifyRequestSessionToken() checks the session token’s ES256 signature against /.well-known/jwks.json, its audience (your client_id), issuer and expiry — on every request. See Session tokens.
  2. withSessionClient() looks up stored tokens for that token’s business and user (bid and sub). The first time a pair shows up, it exchanges the session token for an access and refresh token and stores them; afterwards it reuses them, refreshing when the access token is within a minute of expiry. A session token can be exchanged only once, and App Bridge sends the same one with every request for up to about 50 seconds — which is why the exchange happens once per pair, not once per request.
  3. If a refresh fails, or the API answers 401 (the stored tokens were revoked), it drops the pair and exchanges the request’s session token instead of failing from then on.
  4. Only one renewal — refresh or exchange — runs at a time per business and user. A page’s parallel app.fetch() calls carry the same session token, and only one exchange of it can succeed, so concurrent first requests share a single exchange, and parallel requests that hit the same 401 share one re-exchange. After a 401, the stored pair is dropped only if it’s still the one the API rejected, and a failed renewal isn’t remembered — the next request simply tries again.
  5. It runs the route with a OneBooksClient for that access token.

The rules live in lib/session-tokens.mjs as plain functions, so you can lift them into your own app. The one-at-a-time rule is kept in memory, so it coordinates requests within one process only — see Before you ship it. Every response also carries a Content-Security-Policy: frame-ancestors … header (lib/csp.mjs), without which browsers refuse to show the pages inside OneBooks.

POST /webhooks reads the raw body, verifies X-Webhook-Signature with verifyWebhook() and ONEBOOKS_WEBHOOK_SECRET, and answers 400 if that fails. On app.uninstalled and business.redact it deletes every stored token for the business. It acknowledges with 200 even when its own handling throws — retrying a delivery can’t fix an application bug.

functions/notify-on-paid.js exports onEvent(event, ctx): on invoice.paid it reads the invoice through ctx.api and logs its number and total. It also shows how to reach an outside service — with the global fetch(), never ctx.api — but ships with that call switched off. Deploy it with:

Terminal window
onebooks functions deploy --app <appId> --name notify-on-paid \
--file functions/notify-on-paid.js --events invoice.paid --activate

(Add --egress <host> when you turn the outside call on — see Hosted functions.)

The template is deliberately small. Before real merchants use code built on it:

  • Store tokens properly. data/tokens.json (file mode 0600) is a stand-in. Use a real database and encrypt tokens at rest — see Authentication.
  • Coordinate renewals across instances. Running more than one instance of the backend, two of them can still refresh or exchange the same pair at once. Serialize renewals per business and user through your database — a row lock, for example — instead of the in-memory rule.
  • Delete everything on business.redact. The example only holds tokens. Your app must delete all of its own data for the business — see Privacy & data handling.
  • Let the merchant enter the value. The action writes a generated number immediately; a real action would show a small form first.
  • Set NODE_ENV=production where you deploy, so only https://app.getonebooks.com may frame your pages.
  • Report errors. The webhook receiver acknowledges even when its own code fails, so ship your own error reporting rather than relying on retries.