@xapps-platform/backend-kit
v0.2.12
Published
Modular Node backend kit for the current Xapps backend contract (tenant surface today, shared actor-adapter direction later)
Readme
@xapps-platform/backend-kit
Modular Node backend kit for the current Xapps backend contract.
Install
npm install @xapps-platform/backend-kitWhen to use it
Use @xapps-platform/backend-kit when you want a higher-level backend assembly with default routes, payment runtime composition, and override seams.
Use @xapps-platform/server-sdk directly only when you need lower-level primitives that the backend kit intentionally does not own.
For the current XMS system behavior and terminology, read:
Current public surface:
- backend composition for the shipped integrator contract
Direction:
- Node and PHP variants should converge on the same backend contract
- actor differences should live in adapters, rights/scope, config, and data access
- not in duplicated platform backend logic
This package sits above:
@xapps-platform/server-sdk
Use it when you want a working backend with default routes, default modes, and override seams, while keeping the later shared tenant/publisher direction open. The current shipped reference consumers are still tenant backends, but the package direction is actor-agnostic where possible.
For request-capable publisher-rendered widgets, use the package layer to verify browser widget context server-side before exposing private runtime behavior:
import {
evaluateWidgetBootstrapOriginPolicy,
verifyBrowserWidgetContext,
} from "@xapps-platform/backend-kit";
const originPolicy = evaluateWidgetBootstrapOriginPolicy({
hostOrigin: request.body?.hostOrigin,
allowedOrigins: config.widgetBootstrap.allowedOrigins,
});
if (!originPolicy.ok) {
reply.code(originPolicy.code === "HOST_ORIGIN_REQUIRED" ? 400 : 403).send({
ok: false,
error: {
code: originPolicy.code,
message: originPolicy.message,
},
});
return;
}
const verified = await verifyBrowserWidgetContext(gatewayClient, {
hostOrigin: originPolicy.hostOrigin,
installationId: "inst_123",
bindToolName: "submit_form",
subjectId: "sub_123",
bootstrapTicket: request.body?.bootstrapTicket ?? null,
});Recommended shared local config contract for publisher-rendered bootstrap routes:
widgetBootstrap.allowedOrigins- optional app env:
XAPPS_WIDGET_ALLOWED_ORIGINS=https://host.example.test,https://host-b.example.test
This stays local/app-owned on purpose. The package helper standardizes the policy behavior without forcing a repo-wide env parser or backend shape.
Recommended request-widget posture:
- treat the widget asset URL as a public/bootstrap shell
- keep request-capable UI blocked until widget context is verified server-side
- do not place private keys, secrets, or durable bearer tokens in
entry.url - direct raw browser hits should stay blocked instead of unlocking private runtime
Optional stronger bootstrap transport already supported:
widgets[].config.xapps.bootstrap_transport = "signed_ticket"- current first slice reuses the short-lived signed widget token as a bootstrap ticket and carries it in the iframe URL hash
- browser widget code can forward it to your backend as
bootstrapTicket - the backend kit verification passthrough accepts that field without changing the current default/public bootstrap contract
This is now a real package, not a placeholder or extraction stub. Keep the public entry surface stable and split internal package code behind it.
Current package shape:
- TypeScript source in
src/* - ESM Node outputs in
dist/* - public package exports stay stable while internal modules remain split
Start Here
Consumer rule:
- import
@xapps-platform/backend-kit - import package entry surfaces such as
@xapps-platform/backend-kit/backend/... - do not consume raw
src/...files directly from apps
Minimal usage
import { createBackendKit } from "@xapps-platform/backend-kit";
const backendKit = await createBackendKit(
{
gateway: { baseUrl: "https://gateway.example.test", apiKey: "gateway_key" },
host: { allowedOrigins: ["https://tenant.example.test"] },
payments: { enabledModes: ["gateway_managed"] },
reference: { hostSurfaces: [{ key: "single-panel", label: "Single Panel" }] },
},
deps,
);What It Gives You
The current package surface provides:
- backend-kit option normalization
- backend-kit composition
- default route surface
- default mode tree
- payment runtime assembly
- higher-level XMS purchase workflow helpers
- host-proxy service assembly
- request-widget bootstrap verification passthrough
- subject-profile sourcing hooks
Internal package structure is intentionally modular:
src/index.tsthin public facadesrc/backend/options.tsoption normalization and config shapingsrc/backend/paymentRuntime.tspayment runtime assembly and payment-page API helperssrc/backend/xms.tshigher-level XMS purchase workflow helpers on top of the server SDKsrc/backend/modules.tsbackend module compositionsrc/backend/modes/*explicit default mode treesrc/backend/routes/*explicit default route tree
Current route surface includes:
- health
- reference
- host core
- lifecycle
- current-user host monetization lifecycle under
/api/my-xapps/:xappId/...- includes
/api/my-xapps/:xappId/monetization/historyfor recent current-user XMS audit buckets
- includes
- bridge
- payment
- guard
- subject profiles
Current workflow helpers also include:
normalizeXappMonetizationScopeKind(...)normalizes subject / installation / realm scope selectionresolveXappMonetizationScope(...)resolves scope fields from runtime context plus optional realm referenceresolveXappHostedPaymentDefinition(...)resolves a manifest payment definition into hosted session config, including delegated signing metadatalistXappHostedPaymentPresets(...)shapes manifest payment definitions into generic hosted-lane preset options for UI selectorsfindXappHostedPaymentPreset(...)looks up one hosted-lane preset bypaymentGuardRefreadXappMonetizationSnapshot(...)reads the common app-facing XMS state bundle: access, current subscription, entitlements, and wallet accountsbuildXappMonetizationReferenceSummary(...)interprets the current-user XMS snapshot plus projected paywall packages into:- primary recurring membership
- owned additive unlocks
- available additive unlocks
- recurring options
- credit top-ups
- blocked packages
- host-mounted plans surfaces can now also consume the recent current-user history bundle exposed through:
/api/my-xapps/:xappId/monetization/history- portal
/v1/me/xapps/:xappId/monetization/history - embed
/embed/my-xapps/:xappId/monetization/history
consumeXappWalletCredits(...)consumes credits from one wallet account through the XMS API and returns the updated wallet, ledger entry, and refreshed access projectionstartXappHostedPurchase(...)prepares a purchase intent and creates the lane-bootstrapped gateway payment sessionfinalizeXappHostedPurchase(...)finalizes a hosted purchase through the platform finalize endpoint, returning reconciliation and issued access stateactivateXappPurchaseReference(...)prepares a purchase intent, creates a verified reference transaction, and issues access
For hosted-integrator mode, the host API surface can also enforce explicit
frontend-origin allowlists through host.allowedOrigins. Leave it empty for
same-origin consumers; set it when the browser host runs on another domain and
must call the tenant backend cross-origin. In practice that allowlist covers
the browser-facing host API surface, including:
/api/host-config/api/resolve-subject/api/create-catalog-session/api/create-widget-session/api/widget-tool-request- lifecycle routes under
/api/install* - current-user XMS host routes under
/api/my-xapps/:xappId/... - bridge routes under
/api/bridge/*
For the secure long-term hosted-integrator path, use a short-lived bootstrap token instead of trusting browser subject input:
- browser calls local
POST /api/browser/host-bootstrap - local backend forwards to tenant
POST /api/host-bootstrap - tenant backend authenticates with
X-API-Key - tenant backend resolves the subject through the gateway/host proxy
- tenant backend returns a short-lived
bootstrapToken - browser host uses that token only for
POST /api/host-session/exchange - backend-kit mints the host session cookie
- ongoing hosted API calls use the host session cookie
In the current design, the tenant backend signs that bootstrap token locally. The gateway participates in subject resolution, but does not issue the browser bootstrap token itself.
That token should be treated as temporary bootstrap state. After expiry, the
frontend should re-run bootstrap instead of trying to continue on stored
subjectId alone.
Recommended host config for that mode:
host.allowedOriginshost.bootstrap.apiKeyshost.bootstrap.signingSecret- optional
host.bootstrap.signingKeyId - optional
host.bootstrap.verifierKeys - optional
host.bootstrap.ttlSeconds
For the stronger host-session exchange posture, also configure:
host.session.signingSecret- optional
host.session.signingKeyId - optional
host.session.verifierKeys host.session.absoluteTtlSeconds- optional
host.session.idleTtlSeconds host.session.cookiePath(recommended/api)- optional
host.session.cookieDomain host.session.cookieSameSitehost.session.cookieSecure- required when
host.session.idleTtlSeconds > 0:host.session.store.activate - required when
host.session.idleTtlSeconds > 0:host.session.store.touch - required
host.session.store.isRevoked - required
host.session.store.revoke - optional
host.bootstrap.rateLimitBootstrap - optional
host.bootstrap.auditBootstrap - optional
host.bootstrap.deprecatedWarn - optional
host.session.rateLimitLogout - optional
host.session.auditLogout - optional
host.session.auditRevocation
Minimal preferred session-store shape:
host: {
bootstrap: {
apiKeys: ["bootstrap_key_123"],
signingSecret: "bootstrap_secret_123",
},
session: {
signingSecret: "session_secret_123",
absoluteTtlSeconds: 1800,
idleTtlSeconds: 900,
store: {
activate: ({ jti, subjectId, exp, idleTtlSeconds }) => true,
touch: ({ jti, subjectId, exp, idleTtlSeconds }) => ({ active: true }),
isRevoked: ({ jti }) => false,
revoke: ({ jti }) => true,
},
},
}Generic file-backed helpers are also available when you want backend-owned state without reimplementing the file locking and JSON persistence:
import {
createFileHostBootstrapReplayConsumer,
createFileHostSessionStore,
} from "@xapps-platform/backend-kit";
const consumeJti = createFileHostBootstrapReplayConsumer({
replayFile: "/tmp/xapps/host-bootstrap-replay.json",
});
const store = createFileHostSessionStore({
stateFile: "/tmp/xapps/host-session-state.json",
revocationsFile: "/tmp/xapps/host-session-revocations.json",
});Redis-backed helpers are available for multi-replica/shared-state deployments:
import {
createRedisHostBootstrapReplayConsumer,
createRedisHostSessionStore,
} from "@xapps-platform/backend-kit";
import Redis from "ioredis";
const redis = new Redis(process.env.REDIS_URL || "redis://127.0.0.1:6379");
const consumeJti = createRedisHostBootstrapReplayConsumer({
client: redis,
keyPrefix: "xapps:host",
});
const store = createRedisHostSessionStore({
client: redis,
keyPrefix: "xapps:host",
});Redis key layout:
xapps:host:bootstrap:jti:{jti}xapps:host:session:state:{jti}xapps:host:session:revoked:{jti}
Important rule:
absoluteTtlSecondsis the real cookie/session lifetime baselinehost.session.signingSecretshould be distinct fromhost.bootstrap.signingSecretsigningKeyId+verifierKeysenable additive key rotation; the currentsigningSecretremains the active signer, andverifierKeyscan carry older verification keys bykididleTtlSecondsis only meaningful when bothhost.session.store.activateandhost.session.store.touchare configured against backend-owned session state- cross-origin hosted API routes should rely on the host session cookie, not bootstrap proof, after exchange
Host auth classes:
- browser bootstrap entry
POST /api/browser/host-bootstrap- browser-safe local renewal/bootstrap route
- tenant bootstrap operation
POST /api/host-bootstrap- server-side
X-API-Key
- host control-plane
- host session cookie
- catalog/session/lifecycle/advanced bridge routes
- token-scoped execution-plane
- gateway-issued widget/access token
- widget tool execution and current-user monetization routes
Execution-plane rule:
- prefer
Authorization: Bearer <token>on host execution-plane routes - query/body token input should be treated as compatibility input, not the preferred contract
- when
host_session_jtiis present, execution tokens should also carryhost_session_bound: trueso verifiers can enforce host-session claims only for bound tokens - when execution tokens carry
host_session_jti, backend-kit performs best-effort revoke propagation toPOST /v1/host-sessions/revocationsduring host-session logout when gateway client config is present - manual revoke propagation to the same endpoint remains available for non-logout revocation sources
This split is intentional. Backend kit should not turn the tenant backend into a generic gateway proxy. Host session protects the hosted control-plane. Scoped widget/access tokens protect execution-plane flows that already run on gateway-issued runtime tokens.
Current default mode tree includes:
gateway_managedtenant_delegatedpublisher_delegatedowner_managed
For owner_managed, the packaged default can run tenant-owned or
publisher-owned. Use payments.ownerIssuer when the backend should default the
owner-managed lane to publisher instead of tenant when the guard config
does not narrow the issuer explicitly.
If the owner-managed payment page redirects back to an integrator frontend on a different domain, include that frontend origin in the payment return URL allowlist as well.
What Should Stay Local
A consuming app should still keep these local when they are actor-specific:
- startup and env/config mapping
- branding and host pages/assets
- actor-specific subject-profile catalogs or resolver hooks
- explicit mode or route overrides
Recommended Consumer Structure
Start small and keep local ownership obvious.
tenant-backend/
server.js
lib/
config.js
appSurfaceModule.js
subjectProfiles/defaultProfiles.js
routes/
host.js
host/
pages.js
shared.js
modes/
index.js
*/README.md
public/
branding-assets...Recommended override order:
- backend-kit options
- local branding/assets and subject-profile data
- injected services or resolver hooks
- explicit route or mode overrides
Do not copy package default route implementations into the app just to mirror the package layout.
When To Drop Lower
Use @xapps-platform/server-sdk directly only when the consumer needs a lower-level
seam that the backend kit intentionally does not own.
Rule
Do not add route-level wrapper aliases here.
Keep the public surface:
- module oriented
- config driven
- hook based
Keep internals:
- explicit
- modular
- safe to refactor behind the stable entry surface
Node and PHP should keep the same backend behavior in the end. Differences should be runtime-adapter concerns, not separate platform feature lines.
Verify locally
npm run build --workspace packages/backend-kit
npm run smoke --workspace packages/backend-kit