xapps-platform / xapps-backend-kit
Modular PHP backend kit for the current Xapps backend contract (tenant surface today, shared actor-adapter direction later)
Requires
- php: ^8.2
- xapps-platform/xapps-php: ^0.2.11
README
Modular PHP backend kit for the current Xapps backend contract.
Install
composer require xapps-platform/xapps-backend-kit
This package depends on xapps-platform/xapps-php and sits above it.
For the current XMS system behavior and API/package reader path, read:
Use it when you want a higher-level packaged backend contract with default routes, mode assembly, payment runtime composition, and override seams.
Use xapps-platform/xapps-php directly only when you need lower-level PHP primitives that the backend kit intentionally does not own.
Current public surface:
- backend composition for the shipped backend contract
Direction:
- PHP and Node 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/xapps-php
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.
For request-capable publisher-rendered widgets, use the package layer to verify browser widget context server-side before exposing private runtime behavior:
$originPolicy = BackendKit::evaluateWidgetBootstrapOriginPolicy([ 'hostOrigin' => $request['body']['hostOrigin'] ?? null, 'allowedOrigins' => $config['widgetBootstrap']['allowedOrigins'] ?? [], ]); if (!($originPolicy['ok'] ?? false)) { http_response_code(($originPolicy['code'] ?? '') === 'HOST_ORIGIN_REQUIRED' ? 400 : 403); echo json_encode([ 'ok' => false, 'error' => [ 'code' => $originPolicy['code'] ?? 'HOST_ORIGIN_NOT_ALLOWED', 'message' => $originPolicy['message'] ?? 'Widget bootstrap origin rejected', ], ]); return; } $verified = BackendKit::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 framework-specific env contract.
Recommended request-widget posture:
- keep the widget asset URL as a public/bootstrap shell
- keep request-capable UI blocked until backend verification succeeds
- do not place secrets or durable bearer tokens in manifest widget URLs
- direct raw browser hits should stay blocked instead of exposing 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 PHP backend-kit 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.
Start Here
Consumer rule:
- prefer Composer autoload plus the package file autoload
- use the package entry surface
- do not wire individual
src/...files directly in consuming apps
Example:
require_once __DIR__ . "/vendor/autoload.php"; use Xapps\BackendKit\BackendKit;
Packaged consumers should prefer Composer autoload plus the package file
autoload. Repo-local consumers can keep loading src/functions.php directly.
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/BackendKit.phpthin public facadesrc/Backend/Support.phpsmall shared value helperssrc/Backend/Options.phpoption normalization and config shapingsrc/Backend/Runtime.phpplain-PHP request/bootstrap helperssrc/Backend/PaymentRuntime.phppayment runtime assembly and payment-page API helperssrc/Backend/Xms.phphigher-level XMS purchase workflow helpers on top of the PHP gateway clientsrc/Backend/HostProxy.phphost-proxy service and plain-app creationsrc/Backend/Modules.phpbackend 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:
BackendKit::normalizeXappMonetizationScopeKind(...)normalizes subject / installation / realm scope selectionBackendKit::resolveXappMonetizationScope(...)resolves scope fields from runtime context plus optional realm referenceBackendKit::resolveXappHostedPaymentDefinition(...)resolves a manifest payment definition into hosted session config, including delegated signing metadataBackendKit::listXappHostedPaymentPresets(...)shapes manifest payment definitions into generic hosted-lane preset options for UI selectorsBackendKit::findXappHostedPaymentPreset(...)looks up one hosted-lane preset bypaymentGuardRefBackendKit::readXappMonetizationSnapshot(...)reads the common app-facing XMS state bundle: access, current subscription, entitlements, and wallet accountsBackendKit::buildXappMonetizationReferenceSummary(...)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
BackendKit::consumeXappWalletCredits(...)consumes credits from one wallet account through the XMS API and returns the updated wallet, ledger entry, and refreshed access projectionBackendKit::startXappHostedPurchase(...)prepares a purchase intent and creates the lane-bootstrapped gateway payment sessionBackendKit::finalizeXappHostedPurchase(...)finalizes a hosted purchase through the platform finalize endpoint, returning reconciliation and issued access stateBackendKit::activateXappPurchaseReference(...)prepares a purchase intent, creates a verified reference transaction, and issues access
Verify locally
php packages/xapps-backend-kit-php/test/run.php php packages/xapps-backend-kit-php/examples/smoke/smoke.php
Minimal usage
<?php require_once __DIR__ . "/vendor/autoload.php"; use Xapps\BackendKit\BackendKit;
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.
For hosted-integrator mode, the PHP kit mirrors the same secure bootstrap shape as the Node variant:
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' => static fn (array $input): bool => true, 'touch' => static fn (array $input): array => ['active' => true], 'isRevoked' => static fn (array $input): bool => false, 'revoke' => static fn (array $input): bool => true, ], ], ],
Generic file-backed helpers are also available when you want backend-owned state without reimplementing the file locking and JSON persistence:
$consumeJti = BackendKit::createFileHostBootstrapReplayConsumer([ 'replayFile' => sys_get_temp_dir() . '/xapps-host-bootstrap-replay.json', ]); $store = BackendKit::createFileHostSessionStore([ 'stateFile' => sys_get_temp_dir() . '/xapps-host-session-state.json', 'revocationsFile' => sys_get_temp_dir() . '/xapps-host-session-revocations.json', ]);
Redis-backed helpers are available for multi-replica/shared-state deployments:
$redis = new \Redis(); $redis->connect('127.0.0.1', 6379); $consumeJti = BackendKit::createRedisHostBootstrapReplayConsumer([ 'client' => $redis, 'keyPrefix' => 'xapps:host', ]); $store = BackendKit::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
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. The PHP 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.
The tenant backend resolves subject through the gateway/host proxy and signs the short-lived browser bootstrap token locally. Raw platform API keys stay on the integrator backend or tenant backend, never in browser code.
That host.allowedOrigins 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/*
Hosted-integrator session expectations are the same as in the Node backend kit:
- browser hosts use a short-lived
bootstrapTokenonly forPOST /api/host-session/exchange - backend kit mints the host session cookie
- ongoing hosted API calls use the host session cookie
- widget sessions renew through
/api/bridge/token-refresh - bootstrap renewal should re-run bootstrap instead of treating
subjectIdalone as durable proof - terminal widget-session failure should surface at the host shell layer, not as a raw iframe error
The PHP runtime should therefore be documented and reasoned about as an implementation variant of the same hosted-integrator/session contract, not as a separate feature line.
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
Keep the local PHP adapter thin and predictable.
tenant-backend/
bootstrap.php
public/index.php
lib/
config.php
appSurfaceModule.php
subjectProfiles/defaultProfiles.php
routes/
host/
pages.php
shared.php
modes/
README.md
*/README.md
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 wire individual package route files directly in the app just to recreate the packaged backend surface locally.
When To Drop Lower
Use xapps-platform/xapps-php directly only when the consumer needs a lower-level PHP
primitive 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
composer smoke