mountInstall adds the
platform-facing routes and does the crypto; you only say what to do at each step. See
Concepts & lifecycle for where this sits in the bigger picture.
What the platform calls
mountInstall mounts up to three routes on your app:
The two
install routes are authenticated with a one-time provision token, not a session token. That
token is signed by the platform, carries your app’s aud, and carries a jti that blocks replay. You never
parse it by hand — mountInstall verifies it and, crucially, only lets the workspace come from the verified
token. A caller can never provision or purge a workspace it wasn’t minted a token for.
Wire up the lifecycle
Here’s the whole thing, straight from the starter’sapi/src/app.ts:
verifier and cypher come from your config.ts — the verifier is bound to the platform’s Ed25519
public key and your manifest audience, and the cypher encrypts the secrets you store. The rest is four hooks.
Let’s take them one at a time.
Store the key on provision
onProvision runs once, when the merchant installs. It receives the workspace and the freshly minted API
key:
createCypher) is AES-256-GCM keyed on
your app’s own secret, so a stored key is useless to anyone who doesn’t hold that secret:
await cypher.decryptSecret(row.api_key_enc). See Calling the API.
The webhook secret
If your manifest declareswebhooks.events, the platform also delivers a webhookSecret in the same call —
the signing secret you’ll check deliveries against. Store it exactly the same way, encrypted, per workspace:
webhookSecret is absent for apps that don’t subscribe to events, which is why it’s optional. If you use
webhooks, verify each delivery with that stored secret — see Webhooks.
Why the jti is consumed after your work
The provision token is one-time. Ajti on it — a unique id — blocks replay: the first time you see a given
jti you accept it, and you reject any repeat. That’s what consumeJti and releaseJti are:
onProvision work
fails — a locked database, a downstream hiccup — a naive design would strand the workspace forever. The
platform retries, but it retries with the same token, which you’ve already burned. The merchant is now
installed with no key stored, and there’s no way to recover.
The SDK closes that gap. It consumes the jti only after your onProvision succeeds, and releases it if
your hook throws. So a transient failure just means the platform’s retry works cleanly:
- Verify the token (validate only — the
jtiis not consumed yet). - Reject early if
apiKeyis missing, before touching thejti. - Consume the
jti. If it’s already used, reply409— a replay. - Run your
onProvision. If it throws, callreleaseJtiand reply500. - On success, the
jtistays consumed and the workspace is provisioned.
releaseJti is optional, but provide it. Without it, one transient error during provisioning permanently
strands a workspace. The starter wires it up for you.Purge on uninstall
onUninstall runs when the merchant removes your app. The API key you were given is being revoked, so hold
onto nothing:
Optional: config from the embed
There’s a fourth hook,onConnect, for when your embed UI needs to send you app-specific config — an
external account id the merchant types in, a toggle, a mapping. Unlike the two install routes, this one is
session-authed: the merchant’s embed calls it with a session token, so again the workspace comes from the
token, not the body.
Where to next
- Concepts & lifecycle — the trust model this fits into.
- The manifest — declare your scopes, and
webhooks.eventsto receive a webhook secret. - Webhooks — verify the events you subscribe to with the delivered secret.
- Calling the API — decrypt the stored key and act as the workspace.

