Audience: Developer
template.appapp?: {
menu: { label: string };
settings: AppSettingDecl[];
packageKey?: string; // slug for pack-loaded /api/app handlers
menuItems?: AppMenuItemDecl[]; // extra LokAkrt children, snapshotted on Install
} | null
menu.label non-empty) makes the pack an app template.settings may be an empty array (menu-only apps are allowed; none required in seeds).packageKey is a stable slug (^[a-z][a-z0-9-]{0,62}$), snapshotted on Install. Required when the catalog seed ships a pack (pack.manifest.json + server/).type still derives from page count (catalogTypeFromPages).| Field | Notes |
|---|---|
key |
Stable id (e.g. guest.enabled) |
type |
boolean | string | number | array |
itemType? |
Required when type is array (boolean | string | number) |
label |
UI label |
description? |
Help text |
default |
Required; must match type |
client? |
When true, value may appear on public/hosted project definition |
secret? |
UI password + redaction. secret + client is invalid |
Codec rules: fill defaults, coerce types, reject unknown keys, validate duration-like TTL keys (*ExpiresIn), clientFilter for public masks. Max 32 fields. Secret values never appear on the public project definition.
Extra LokAkrt children under a bound app (max 8). v1 type is only payment-config. Unknown types, duplicate key, or more than one payment-config fail install/publish.
interface AppMenuItemDecl {
key: string; // ^[a-z][a-z0-9-]{0,62}$
label: string;
type: 'payment-config';
}
Fashion Store declares Payments:
"menuItems": [{ "key": "payments", "label": "Payments", "type": "payment-config" }]
Sidebar for a bound project: Settings (platform) + Plugins (platform). Declared menuItems such as Payments (payment-config) are stored on the bind, but menu-options.service currently filters the side menu to type === 'settings' only — operators open payment credentials under Plugins → PayPal (etc.), not via an App → Payments child. An unbound project shows top-level Plugins only.
Optional pack-root details/ (must include details/index.html) plus relative media (css, png, jpg, webp, mp4, …). The LokAkrt Plugins list and template gallery show Details when hasDetails is true.
Public GET /api/pack-details/:kind/:packageKey/* (rate-limited, no JWT — an iframe src cannot send Bearer). HTML responses set a sandbox CSP. The LokAkrt iframe uses sandbox without allow-scripts or allow-same-origin. Do not put credentials or secrets in details/.
Payment plugin pack scripts copy production node_modules from package.json dependencies into the .tar.gz (dereferenced, no .bin). The LokAkrt host does not run npm install at boot. Fashion Store does not pack @lokakrt/sdk (the server bundle already includes it). PayPal does pack @paypal/paypal-server-sdk and loads it with a dynamic import() — do not esbuild-bundle that SDK into dist/.
On app Install the platform copies seed server/ → shared/project/{id}/js/{version}/ and, when present, packed node_modules/ next to that JS.
Fashion Store is authored under plugin/template/e-commerce/fashion/ (@lokakrt/template-fashion-store): template.json, media/, pack.manifest.json, and compiled server/. Build emits dist/fashion-store.tar.gz for distribution.
Catalog loading
| Environment | How the pack enters the catalog |
|---|---|
| Local monorepo | Authoring seed at plugin/template/e-commerce/fashion is discovered on boot; LokAkrt publish writes template.json there |
| Docker / prod | Admin uploads the .tar.gz via Add template → Upload app pack (POST /api/template/import) into shared/catalog/template/ |
Page seeds under migration/0.0.0/template/ stay image-baked and read-only. Install copies pack server/ from the catalog seed — it does not extract tar at Install time.
{
"packageKey": "fashion-store",
"version": "0.1.0",
"requiredDataSources": ["products", "guest-sessions"],
"serverEntry": "server/index.js"
}
On Install the platform copies server/ → shared/project/{projectId}/js/{version}/, inserts a project_plugin row, loads routes into memory, then writes properties.app last. Boot loads only project_plugin rows (never scans disk). Pack JS is Admin-imported / platform-built trusted code (path-confined createRequire; not a sandbox). Casual User upload of pack server/ is not supported.
payment.methodspayment.methods (Fashion App → Settings) is the checkout offer list — ordered provider ids (stub and cod by default). Each id must also be enabled as a payment extension on the project (install ∩ this list).
Payment plugin credentials are not app settings. They live on the plugin manifest (settings: AppSettingDecl[]) and in project_extension.settings. The Fashion Payments page type (payment-config) can render a section per enabled payment plugin that declares settings, but that child is not linked in the current side menu. Prefer Plugins → PayPal for credentials. Stub and COD declare none. PayPal declares clientId, clientSecret (secret), and environment (sandbox | live). List/PUT require manage JWT; secret field values never appear on the public project definition.
The host façade assigns pluginSettings last from the project_extension row. Packs must not supply credentials; pack-supplied pluginSettings are stripped.
clientId / clientSecret. Leave environment as sandbox until go-live. Then tick PayPal under Fashion Store → Settings → Payment methods.paypal to payment.methods (keep or drop stub / cod as needed).approveUrl). After approve, PayPal returns to a host-signed GET /api/payment/paypal/return?state=… (TTL ~2 hours, no in-memory session map). The token carries sessionId (PayPal order id), reference, callbackUrl, callbackSecret, projectId, and packageKey. HMAC authenticates; it does not encrypt. PayPal REST credentials are not in the token — return/cancel loads them from project_extension.settings. Capture is not a webhook.Closing the PayPal window after approve can leave checkout requires_action. If the return page or checkout still shows pending, finish the PayPal approval in the browser and retry the return URL. Repeat capture of an already-captured order is treated as success and the merchant callback runs again.
v1 does not include PayPal webhooks, Vault, or Subscriptions.
guest.enabled (client-visible) gates mint. Continuity is a template datastore guest-sessions (deviceKeyHash / guestId uniqueKey). Mint: POST /api/app/:projectId/guest/session. Checkout offer list is payment.methods (server-only; default stub + cod).
{
"app": {
"menu": { "label": "Fashion Store" },
"packageKey": "fashion-store",
"menuItems": [{ "key": "payments", "label": "Payments", "type": "payment-config" }],
"settings": [
{
"key": "guest.enabled",
"type": "boolean",
"label": "Enable guest tracking",
"default": true,
"client": true
},
{
"key": "guest.tokenExpiresIn",
"type": "string",
"label": "Guest token TTL",
"default": "720h"
},
{
"key": "payment.methods",
"type": "array",
"itemType": "string",
"label": "Payment methods",
"default": ["stub", "cod"]
}
]
}
}
Omit app. Keep pages + properties.theme for Add Page / multipage scaffold. Brand is not applied via Install.
Omit app. Multipage/demo datasets stay on the page template; Add Page / scaffold never bind or write project theme.
On Install, project.properties.app stores:
templateId, templateName, menuLabel, settingsSchema, settings, optional packageKey, optional menuItems
menuItems freeze at install like settingsSchema. Existing binds do not grow a Payments item until reinstall — same as other bind fields.
Settings remain snapshotted. No Sync / schemaHash.
Author contract: do not rename dataSource API names listed in the pack manifest requiredDataSources after Install.