@proveanything/smartlinks 2.0.10 → 2.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,702 +1,702 @@
1
- # App Configuration Files: `app.manifest.json` & `app.admin.json`
2
-
3
- Every SmartLinks app ships with two JSON configuration files that the platform reads to understand what the app is and how to configure it. They have clearly separated responsibilities:
4
-
5
- | File | Role | Loaded by |
6
- |------|------|-----------|
7
- | `app.manifest.json` | **Definitional** — what the app *is*: its bundles, components, static routes | Platform on every page load; portals; AI orchestrators |
8
- | `app.admin.json` | **Operational** — how to *set up and tune* the app: setup questions, import schemas, tunable fields, metrics | Admin UI, AI-assisted setup flows |
9
-
10
- The manifest always references the admin config via its `admin` field. Consumers that only need to *render* the app work entirely from the manifest. Only admin/setup flows need to fetch `app.admin.json`.
11
-
12
- ```text
13
- ┌─────────────────────────────────────────────────────────────────┐
14
- │ Platform boot sequence │
15
- │ │
16
- │ 1. GET /collection/:id/widgets │
17
- │ └─→ CollectionWidgetsResponse { apps: [...] } │
18
- │ each app has: manifest, widget bundle, container │
19
- │ │
20
- │ 2. manifest.admin ──→ "app.admin.json" (pointer only) │
21
- │ │
22
- │ 3. Admin UI fetches app.admin.json when setup/config needed │
23
- └─────────────────────────────────────────────────────────────────┘
24
- ```
25
-
26
- ---
27
-
28
- ## `app.manifest.json`
29
-
30
- The manifest is loaded automatically by the platform for every collection page. Keep it lean — it is fetched on every widget render.
31
-
32
- ### Full Schema
33
-
34
- ```json
35
- {
36
- "$schema": "https://smartlinks.app/schemas/app-manifest-v1.json",
37
-
38
- "meta": {
39
- "appId": "my-app",
40
- "name": "My App",
41
- "description": "A short human-readable description of what this app does.",
42
- "version": "1.2.0",
43
- "platformRevision": "R5",
44
- "moduleFormat": "dual",
45
- "sharedDependencies": "v5"
46
- },
47
-
48
- "admin": "app.admin.json",
49
-
50
- "widgets": {
51
- "instanceResolution": true,
52
- "instanceParam": "widgetId",
53
- "files": {
54
- "js": {
55
- "umd": "dist/widgets.umd.js",
56
- "esm": "dist/widgets.esm.js"
57
- },
58
- "css": "dist/widgets.css"
59
- },
60
- "components": [
61
- {
62
- "name": "SummaryWidget",
63
- "description": "Compact summary card for use on product pages.",
64
- "sizes": ["compact", "standard"],
65
- "props": {
66
- "required": ["collectionId", "appId"],
67
- "optional": ["productId", "proofId"]
68
- },
69
- "settings": {
70
- "showImage": { "type": "boolean", "default": true }
71
- }
72
- }
73
- ]
74
- },
75
-
76
- "containers": {
77
- "files": {
78
- "js": {
79
- "umd": "dist/containers.umd.js",
80
- "esm": "dist/containers.esm.js"
81
- },
82
- "css": "dist/containers.css"
83
- },
84
- "components": [
85
- {
86
- "name": "FullApp",
87
- "description": "Full public app experience with internal routing.",
88
- "props": {
89
- "required": ["collectionId", "appId"],
90
- "optional": ["productId", "proofId", "className"]
91
- }
92
- }
93
- ]
94
- },
95
-
96
- "mobileAdmin": {
97
- "files": {
98
- "js": {
99
- "umd": "dist/mobile-admin.umd.js",
100
- "esm": "dist/mobile-admin.esm.js"
101
- },
102
- "css": null
103
- },
104
- "components": [
105
- {
106
- "name": "WarehousePickContainer",
107
- "description": "In-field operator admin surface.",
108
- "capabilities": ["nfc", "qr"],
109
- "offline": true
110
- }
111
- ]
112
- },
113
-
114
- "linkable": [
115
- { "title": "Home", "path": "/" },
116
- { "title": "Gallery", "path": "/gallery" },
117
- { "title": "Settings", "path": "/settings", "params": { "tab": "advanced" } }
118
- ],
119
-
120
- "records": {
121
- "nutrition": {
122
- "label": "Nutrition info",
123
- "cardinality": "singleton",
124
- "allowFacetRules": true,
125
- "scopes": ["collection", "rule", "product", "facet", "batch"],
126
- "defaultScope": "product"
127
- },
128
- "cooking_steps": {
129
- "label": "Cooking steps",
130
- "cardinality": "singleton",
131
- "allowFacetRules": false,
132
- "scopes": ["collection", "product"],
133
- "defaultScope": "product"
134
- }
135
- }
136
- }
137
- ```
138
-
139
- ### Field Reference
140
-
141
- #### `meta`
142
-
143
- | Field | Type | Required | Description |
144
- |-------|------|----------|-------------|
145
- | `appId` | string | ✅ | Unique identifier for the app (slug-style, e.g. `"warranty-tracker"`) |
146
- | `name` | string | ✅ | Human-readable display name |
147
- | `description` | string | ❌ | Short description shown in app directories and AI context |
148
- | `version` | string | ✅ | SemVer string, e.g. `"1.2.0"` |
149
- | `platformRevision` | string | ❌ | Platform revision tag this build targets, e.g. `"R5"` (see [host-dependency-contract.md](host-dependency-contract.md)) |
150
- | `moduleFormat` | `"umd"` \| `"esm"` \| `"dual"` | ❌ | How the host loads this app's bundles. Absent = `"umd"`. See [Module format](#module-format-umd-vs-esm) below. |
151
- | `sharedDependencies` | string | ❌ | Shared-dependency contract version the bundle was built against, e.g. `"v5"`. Used by the host to pick a compatible ESM import map. |
152
- | `globals` | object | ❌ | Per-app namespaced UMD globals (R4.7+), e.g. `{ "widgets": "MyAppWidgets" }`. UMD-only; ESM bundles don't need it. |
153
- | `seo.priority` | number | ❌ | Controls which app's `title`/`description`/`ogImage` wins when multiple apps are on the same page. Default `0`; higher wins. See the [Executor guide](executor.md). |
154
-
155
- #### Module format (UMD vs ESM)
156
-
157
- The host provides shared libraries (React, Radix, the SmartLinks SDK, LiquidJS…) as **singletons**
158
- so apps never bundle their own. Two delivery mechanisms exist, and `meta.moduleFormat` tells the host
159
- which to use:
160
-
161
- | `moduleFormat` | Host behaviour |
162
- |---|---|
163
- | `"umd"` *(default)* | Loads `files.js.umd` via the CommonJS `require` shim; shared deps resolve from **window globals**. Every existing app works unchanged. |
164
- | `"dual"` | Prefers `files.js.esm` when the host has an **import map** for the declared `sharedDependencies` version; **falls back to UMD** otherwise. The safe transition setting. |
165
- | `"esm"` | Loads `files.js.esm` **natively**; if the host has no matching import map it fails with an actionable error rather than a bare-specifier crash. Use only once you know your hosts are on the contract. |
166
-
167
- The ESM bundle is declared in the **same** `files.js` block as `esm` (there is no separate `jsEsm`
168
- field):
169
-
170
- ```jsonc
171
- "widgets": {
172
- "files": {
173
- "js": { "umd": "dist/widgets.umd.js", "esm": "dist/widgets.esm.js" },
174
- "css": "dist/widgets.css"
175
- }
176
- }
177
- ```
178
-
179
- An ESM bundle **must externalize exactly the shared-dependency contract** — read it from the SDK
180
- (`SHARED_DEPENDENCY_SPECIFIERS`) rather than hard-coding it, and stamp the version you built against
181
- into `meta.sharedDependencies`. See [host-dependency-contract.md](host-dependency-contract.md).
182
-
183
- ##### Validate before you ship: `smartlinks doctor`
184
-
185
- Run the checker (shipped with the SDK) against your built app — it reads the same contract the host
186
- serves, so the two can't drift:
187
-
188
- ```bash
189
- npx smartlinks-doctor # in the app dir, after building
190
- ```
191
-
192
- It reads your manifest, and for every ESM surface confirms **every bare import in the bundle is a
193
- contract entry**. A correctly-externalized ESM bundle inlines everything except the host singletons,
194
- so anything else left as a bare import will either fail to resolve through the import map or silently
195
- double-load (the duplicate-React class of bug). It also warns when a UMD app still declares stale
196
- `*.es.js`/`*.esm.js` bundles the host will never load. Exit code is non-zero on violations, so it
197
- drops straight into CI.
198
-
199
- #### `build`
200
-
201
- Optional build provenance. Recommended for the **Lovable dev publish** flow: stamp the content
202
- hash your build already produces here so the platform can tell when a new version is live before it
203
- registers (see [Deploying & registering](deploying-apps.md#a-from-lovable--hit-publish-no-key-anywhere-recommended-for-lovable-apps)).
204
-
205
- | Field | Type | Description |
206
- |-------|------|-------------|
207
- | `build.hash` | string | Unique build/content hash — the platform waits for this to appear before registering, and uses it as the dev release version |
208
- | `build.at` | string | ISO timestamp of the build (optional) |
209
-
210
- ```json
211
- "build": { "hash": "a1b2c3d4", "at": "2026-09-20T10:00:00Z" }
212
- ```
213
-
214
- #### `admin`
215
-
216
- A relative path (from the app's public root) to the `app.admin.json` file. Omit entirely if the app has no admin UI.
217
-
218
- ```json
219
- "admin": "app.admin.json"
220
- ```
221
-
222
- #### `widgets`
223
-
224
- Declares the widget bundle. Omit if the app has no widget component.
225
-
226
- | Field | Description |
227
- |-------|-------------|
228
- | `files.js.umd` | UMD bundle path — used for dynamic `<script>` loading |
229
- | `files.js.esm` | ESM bundle path — used for `import()` / native ES modules (optional but recommended) |
230
- | `files.css` | CSS bundle path — omit if the widget ships no styles |
231
- | `instanceResolution` | Optional boolean. When `true`, this app supports resolving configured widget instances by ID from app config |
232
- | `instanceParam` | Optional string. Query/hash param used for instance lookup. Defaults to `"widgetId"` |
233
- | `components[]` | One entry per exported widget component (see below) |
234
-
235
- **Widget instance resolution**
236
-
237
- Apps such as widget toolkits often store reusable widget instances in collection-scoped app config, for example under `config.widgets.launch-countdown`. When your widget bundle can self-configure from one of those stored instances, declare that capability in the manifest:
238
-
239
- ```json
240
- "widgets": {
241
- "instanceResolution": true,
242
- "instanceParam": "widgetId",
243
- "files": {
244
- "js": {
245
- "umd": "dist/widgets.umd.js",
246
- "esm": "dist/widgets.esm.js"
247
- },
248
- "css": null
249
- },
250
- "components": [
251
- {
252
- "name": "WidgetToolkitResolver",
253
- "description": "Resolves and renders a configured widget instance by ID."
254
- }
255
- ]
256
- }
257
- ```
258
-
259
- This tells the platform and other apps that they can deep-link into a stored widget instance using a URL or embed context such as `?appId=widget-toolkit&widgetId=launch-countdown`.
260
-
261
- **Component fields:**
262
-
263
- | Field | Type | Description |
264
- |-------|------|-------------|
265
- | `name` | string | Exported component name (must match the bundle export) |
266
- | `description` | string | Human-readable description for portals and AI |
267
- | `sizes` | string[] | Supported size hints: `"compact"`, `"standard"`, `"large"` |
268
- | `props.required` | string[] | Props that must be provided for the component to render |
269
- | `props.optional` | string[] | Props the component can use if provided |
270
- | `settings` | object | JSON-Schema-style settings the widget accepts from its host |
271
-
272
- #### `containers`
273
-
274
- Same structure as `widgets` but declares the full-app container bundle. Lazy-loaded on demand.
275
-
276
- See the [Containers guide](containers.md) for details on the container component model.
277
- **Component fields** (same as widgets, plus):
278
-
279
- | Field | Type | Description |
280
- |-------|------|--------------|
281
- | `name` | string | Exported component name |
282
- | `description` | string | Human-readable description |
283
- | `props.required` / `props.optional` | string[] | Required and optional prop names |
284
- | `audience` | `"public"` \| `"admin"` \| `"both"` | Who can use/see this component. Defaults to `"public"`. |
285
- | `scope` | `"collection"` \| `"product"` | Data scope hint. `"product"` means the component always renders in the context of a specific product. |
286
- | `settings` | object | JSON Schema describing configurable settings |
287
-
288
- #### `mobileAdmin`
289
-
290
- Declares a **separate** mobile admin bundle — a sibling of `containers` with its own build output. Use this when the mobile admin surface needs a different runtime, native-only dependencies (Capacitor), or independent versioning. Omit if your app has no mobile admin surface.
291
-
292
- See [mobile-admin-container.md](mobile-admin-container.md) for the `AdminMobileHostContext` prop contract, the capability matrix, event stream, error types, and build setup.
293
-
294
- ```json
295
- "mobileAdmin": {
296
- "files": {
297
- "js": {
298
- "umd": "dist/mobile-admin.umd.js",
299
- "esm": "dist/mobile-admin.esm.js"
300
- },
301
- "css": null
302
- },
303
- "components": [
304
- {
305
- "name": "WarehousePickContainer",
306
- "description": "Pick orders by scanning NFC tags",
307
- "capabilities": ["nfc", "qr"],
308
- "offline": true
309
- }
310
- ]
311
- }
312
- ```
313
-
314
- | Field | Description |
315
- |-------|-------------|
316
- | `files.js.umd` | UMD bundle path — used for dynamic `<script>` loading |
317
- | `files.js.esm` | ESM bundle path (optional but recommended) |
318
- | `files.css` | CSS bundle path — set to `null` if no styles |
319
- | `components[].name` | Exported component name (must match the UMD bundle export) |
320
- | `components[].description` | Shown in the mobile launcher's app picker |
321
- | `components[].capabilities` | Hardware capabilities this component needs or can use. See [capability list](mobile-admin-container.md#hardware-capabilities--the-capability-matrix). |
322
- | `components[].offline` | Set to `true` if this component queues writes locally and needs offline sync support. |
323
- #### `linkable`
324
-
325
- Static deep-linkable states built into the app — fixed routes that exist regardless of per-collection content. Declared once at build time.
326
-
327
- See the [Deep Link Discovery guide](deep-link-discovery.md) for the full dual-source pattern (static manifest routes + dynamic `appConfig.linkable`).
328
-
329
- | Field | Type | Required | Description |
330
- |-------|------|----------|-------------|
331
- | `title` | string | ✅ | Human-readable label shown in menus and offered to AI agents |
332
- | `path` | string | ❌ | Hash route within the app (defaults to `"/"` if omitted) |
333
- | `params` | object | ❌ | App-specific query params appended to the URL — do **not** include platform params (`collectionId`, `productId`, etc.) |
334
-
335
- #### `records`
336
-
337
- Declares which `app.records` record types the app stores, and which scopes each type supports. Required for any app that follows the [App Records Pattern](app-records-pattern.md). Omit if the app does not use scoped records.
338
-
339
- The platform and the `<RecordsAdminShell>` from `@proveanything/smartlinks-utils-ui` read this block to render the right scope tabs, rule editor, and cardinality-appropriate right pane.
340
-
341
- ```json
342
- "records": {
343
- "<recordType>": {
344
- "label": "Human-readable label",
345
- "cardinality": "singleton",
346
- "allowFacetRules": false,
347
- "scopes": ["collection", "product", "variant", "batch", "facet"],
348
- "defaultScope": "product"
349
- }
350
- }
351
- ```
352
-
353
- | Field | Type | Default | Description |
354
- |-------------------|----------|---------|-------------|
355
- | `label` | string | — | Human-readable label for the record type, used in headings and tabs. |
356
- | `cardinality` | string | `'singleton'` | `'singleton'` — one record wins per scope (e.g. ingredients, nutrition). `'collection'` — every matching record is returned in resolution order (e.g. FAQs, recipes). Drives which hook to use on the public side (`useResolvedRecord` vs `useCollectedRecords`) and how the shell lays out the right pane. |
357
- | `allowFacetRules` | boolean | `false` | When `true`, the shell renders a **Rule** scope tab and embeds `<FacetRuleEditor>`. Add `'rule'` to `scopes` when setting this. |
358
- | `scopes` | string[] | — | Allowed scope kinds in resolution order. Valid values: `"collection"`, `"product"`, `"variant"`, `"batch"`, `"facet"`, `"proof"`, `"rule"`. `'rule'` is a synthetic scope holding `facetRule`-targeted records. `'collection'` replaces the legacy empty-ref catch-all — **there is no `'global'` scope**. |
359
- | `defaultScope` | string | — | The scope the "Create new" button targets in the admin shell. Must be one of the declared `scopes`. |
360
-
361
- An app may declare multiple record types under different keys (e.g. `"nutrition"` and `"cooking_steps"`). See [app-records-pattern.md](app-records-pattern.md) for the full admin + public pattern.
362
-
363
- #### `executor`
364
-
365
- Declares the executor bundle — a standalone JS library for programmatic configuration, server-side SEO, and LLM content generation. Omit if the app has no executor.
366
-
367
- See the **[Executor Model guide](executor.md)** for the full build setup, SEO contract, LLM content contract, and implementation patterns.
368
-
369
- | Field | Type | Description |
370
- |-------|------|-------------|
371
- | `files.js.umd` | string | UMD bundle path |
372
- | `files.js.esm` | string | ESM bundle path |
373
- | `factory` | string | Name of the factory function that creates an executor instance |
374
- | `exports` | string[] | All named exports — tells consumers what's available without loading the bundle |
375
- | `description` | string | Human-readable summary for AI orchestrators |
376
- | `llmContent.function` | string | Name of the `getLLMContent` export |
377
- | `llmContent.timeout` | number | Timeout in ms (default 500) |
378
-
379
- #### `functions`
380
-
381
- Declares app-authored **server functions** — server-side handlers `(ctx, event) ⇒ result`
382
- (http / event / cron), shipped in a self-contained UMD bundle. Omit if the app has no server
383
- functions. The security model (`visibility`/`authority`/`capabilities`), the runtime surface, how
384
- to build the bundle, and how to invoke are all in the **[Server Functions guide](server-functions.md)** —
385
- this is just the manifest shape.
386
-
387
- | Field | Type | Description |
388
- |-------|------|-------------|
389
- | `files.js.umd` | string | UMD bundle path (e.g. `dist/functions.umd.js`) — a self-contained bundle exporting each handler by name |
390
- | `definitions[]` | object[] | One entry per function |
391
- | `definitions[].name` | string | Function name; also the handler export name (override with `handler`) |
392
- | `definitions[].trigger` | object | `{ type: "http"|"event"|"cron", … }` |
393
- | `definitions[].visibility` | string | `admin` \| `public` (http only) — who may call it |
394
- | `definitions[].authority` | string | `caller` \| `collection` — whose authority it runs as |
395
- | `definitions[].capabilities` | string[] | Least-privilege grants (`sl:<res>:<read\|write>`, `network[:<host>]`, `secrets:<ref>`) |
396
-
397
- ---
398
-
399
- ## `app.admin.json`
400
-
401
- Fetched only by the admin UI and AI-assisted setup flows — never loaded on the public-facing page. Keep setup logic and configuration schemas here, not in the manifest.
402
-
403
- ### Full Schema
404
-
405
- ```json
406
- {
407
- "$schema": "https://smartlinks.app/schemas/app-admin-v1.json",
408
-
409
- "aiGuide": "ai-guide.md",
410
-
411
- "setup": {
412
- "description": "Configure the app for this collection.",
413
- "questions": [
414
- {
415
- "id": "brandName",
416
- "prompt": "What is your brand name?",
417
- "type": "text",
418
- "required": true
419
- },
420
- {
421
- "id": "primaryColor",
422
- "prompt": "Choose a primary theme colour.",
423
- "type": "select",
424
- "options": [
425
- { "value": "blue", "label": "Blue" },
426
- { "value": "green", "label": "Green" },
427
- { "value": "red", "label": "Red" }
428
- ]
429
- },
430
- {
431
- "id": "welcomeEnabled",
432
- "prompt": "Show a welcome message to first-time visitors?",
433
- "type": "boolean",
434
- "default": true
435
- }
436
- ],
437
- "configSchema": {
438
- "brandName": { "type": "string" },
439
- "primaryColor": { "type": "string" },
440
- "welcomeEnabled": { "type": "boolean" }
441
- },
442
- "saveWith": {
443
- "method": "appConfiguration.setConfig",
444
- "scope": "collection",
445
- "admin": true,
446
- "note": "Saved under the collection scope; readable by all app users."
447
- },
448
- "contentHints": {
449
- "welcomeMessage": {
450
- "aiGenerate": true,
451
- "prompt": "Write a short, friendly welcome message for a brand called {{brandName}}."
452
- }
453
- }
454
- },
455
-
456
- "import": {
457
- "description": "Bulk-import items via CSV.",
458
- "scope": "collection",
459
- "fields": [
460
- { "name": "title", "type": "string", "required": true },
461
- { "name": "description", "type": "string" },
462
- { "name": "imageUrl", "type": "string" },
463
- { "name": "price", "type": "number", "default": 0 }
464
- ],
465
- "csvExample": "title,description,imageUrl,price\nWidget A,Our first widget,https://example.com/img.jpg,9.99",
466
- "saveWith": {
467
- "method": "appObjects.createRecord",
468
- "scope": "collection",
469
- "admin": true
470
- }
471
- },
472
-
473
- "tunable": {
474
- "description": "Adjust display options after initial setup.",
475
- "fields": [
476
- {
477
- "name": "displayMode",
478
- "description": "How items are laid out on the page.",
479
- "type": "select",
480
- "options": ["grid", "list", "carousel"]
481
- },
482
- {
483
- "name": "itemsPerPage",
484
- "description": "Number of items shown per page.",
485
- "type": "number"
486
- }
487
- ]
488
- },
489
-
490
- "metrics": {
491
- "interactions": [
492
- { "id": "view", "description": "User viewed an item." },
493
- { "id": "click", "description": "User clicked a link or CTA." },
494
- { "id": "purchase", "description": "User completed a purchase." }
495
- ],
496
- "kpis": [
497
- { "name": "Click-through Rate", "compute": "click / view" },
498
- { "name": "Conversion Rate", "compute": "purchase / view" }
499
- ]
500
- }
501
- }
502
- ```
503
-
504
- > `dynamic-select` widget pickers are a reasonable future extension for admin schemas, but they are not a built-in question type in the SDK today. For now, treat widget-instance selection as an app-level UI convention powered by `appConfiguration.listWidgetInstances()`.
505
-
506
- ### Field Reference
507
-
508
- #### `aiGuide`
509
-
510
- Path (relative to the app's public root) to a Markdown file providing natural-language context for AI-assisted configuration. See the [AI Guide Template](ai-guide-template.md).
511
-
512
- ```json
513
- "aiGuide": "ai-guide.md"
514
- ```
515
-
516
- ---
517
-
518
- #### `setup`
519
-
520
- Drives the initial configuration wizard shown to admins when they first install the app for a collection.
521
-
522
- | Field | Type | Description |
523
- |-------|------|-------------|
524
- | `description` | string | Intro text shown at the top of the setup wizard |
525
- | `questions` | array | Ordered list of questions to ask the admin (see below) |
526
- | `configSchema` | object | JSON-Schema-style shape of the resulting config object |
527
- | `saveWith` | object | Which SDK method and scope to use when persisting answers |
528
- | `contentHints` | object | Keys that AI should auto-generate based on question answers |
529
-
530
- **`questions[]` fields:**
531
-
532
- | Field | Type | Required | Description |
533
- |-------|------|----------|-------------|
534
- | `id` | string | ✅ | Key used in the saved config and in `contentHints` references |
535
- | `prompt` | string | ✅ | Question text displayed to the admin |
536
- | `type` | string | ✅ | Input type: `"text"`, `"number"`, `"boolean"`, `"select"`, `"multiselect"`, `"textarea"` |
537
- | `required` | boolean | ❌ | Whether an answer is mandatory (default `false`) |
538
- | `default` | any | ❌ | Pre-filled default value |
539
- | `options` | array | ❌ | For `select`/`multiselect`: `[{ "value": "...", "label": "..." }]` |
540
-
541
- **`saveWith` fields:**
542
-
543
- | Field | Type | Description |
544
- |-------|------|-------------|
545
- | `method` | string | SDK method to call, e.g. `"appConfiguration.setConfig"` |
546
- | `scope` | string | Data scope: `"collection"`, `"product"`, or `"proof"` |
547
- | `admin` | boolean | Whether the save call requires admin credentials |
548
- | `note` | string | Human-readable note explaining the save behaviour |
549
-
550
- **`contentHints`:**
551
-
552
- A map from content key to AI generation instructions. The AI setup flow uses this to pre-fill content fields after the admin answers setup questions.
553
-
554
- ```json
555
- "contentHints": {
556
- "welcomeMessage": {
557
- "aiGenerate": true,
558
- "prompt": "Write a short welcome message for a brand called {{brandName}}."
559
- }
560
- }
561
- ```
562
-
563
- ---
564
-
565
- #### `import`
566
-
567
- Defines a CSV bulk-import flow available in the admin UI.
568
-
569
- | Field | Type | Description |
570
- |-------|------|-------------|
571
- | `description` | string | Explains what will be imported and how |
572
- | `scope` | string | Data scope for the imported records |
573
- | `fields` | array | Column definitions — `name`, `type`, `required`, `default`, `description` |
574
- | `csvExample` | string | A sample CSV string (shown as a download template) |
575
- | `saveWith` | object | SDK method used to persist each imported row (same shape as `setup.saveWith`) |
576
-
577
- ---
578
-
579
- #### `tunable`
580
-
581
- Post-setup display options that admins can tweak without re-running the full setup wizard.
582
-
583
- | Field | Type | Description |
584
- |-------|------|-------------|
585
- | `description` | string | Explains what these settings control |
586
- | `fields` | array | Tunable parameters — `name`, `description`, `type`, `options[]` |
587
-
588
- ---
589
-
590
- #### `metrics`
591
-
592
- Declares what interactions and KPIs the app reports. Used by the platform's analytics dashboard.
593
-
594
- | Field | Type | Description |
595
- |-------|------|-------------|
596
- | `interactions` | array | Interaction event types: `{ id, description }` |
597
- | `kpis` | array | Derived metrics: `{ name, compute }` — `compute` is a simple expression over interaction IDs |
598
-
599
- ---
600
-
601
- ## Widget settings schema (JSON Schema)
602
-
603
- Each widget component's `settings` object uses **JSON Schema** to describe its configurable props, so schema-form renderers *and* AI orchestrators can auto-generate a config UI without per-widget code:
604
-
605
- ```json
606
- "components": [
607
- {
608
- "name": "MyWidget",
609
- "description": "What this widget does",
610
- "sizes": ["compact", "standard", "large"],
611
- "settings": {
612
- "type": "object",
613
- "properties": {
614
- "displayMode": {
615
- "type": "string", "title": "Display Mode",
616
- "description": "How the widget renders",
617
- "enum": ["compact", "standard", "large"],
618
- "enumLabels": { "compact": "Icons only", "standard": "With names", "large": "Full cards" },
619
- "default": "standard", "order": 1
620
- },
621
- "showImage": { "type": "boolean", "title": "Show Product Image", "default": true, "order": 2 }
622
- }
623
- }
624
- }
625
- ]
626
- ```
627
-
628
- | Field | Purpose |
629
- |-------|---------|
630
- | `type`, `enum` | Standard JSON Schema validation |
631
- | `title` | Human-readable form label |
632
- | `description` | Help text shown alongside the field |
633
- | `enumLabels` | Friendly display names for enum values (`{ value → label }`) |
634
- | `default` | Pre-selected value when no config exists |
635
- | `order` | Field display order (lower number = higher) |
636
-
637
- The schema serves both AI orchestrators (understand what a widget accepts, configure it conversationally) and schema-form renderers (auto-generate settings UIs).
638
-
639
- ## AI workflows the manifest enables
640
-
641
- SmartLinks manifests are **AI-discoverable, -configurable, and -importable**: the structured manifest plus the prose `ai-guide.md` (start from [ai-guide-template.md](ai-guide-template.md)) let AI systems set up and populate apps without custom integration code. Three workflows read the manifest:
642
-
643
- 1. **Widget Builder** — reads `widgets.components[]` and each `settings` schema to embed a widget with correct props and auto-render its configuration UI.
644
- 2. **Setup Wizard** — reads `setup` from `app.admin.json`: walks `setup.questions[]`, validates against `setup.configSchema`, optionally auto-generates content via `SL.ai`, then saves per `setup.saveWith`.
645
- 3. **Data Importer** — reads `import` from `app.admin.json`: builds a CSV template from `import.fields[]`, normalises rows, and calls `import.saveWith.method` per row (multi-app imports merge fields into one CSV).
646
-
647
- When you change your config shape, keep all three in sync: `app.manifest.json` (widget `settings`, containers, executor, linkable), `app.admin.json` (setup / import / tunable), and `ai-guide.md` (prose guidance).
648
-
649
- This declarative model is canonical and works today. An app may **optionally** layer agent tools on top — a function that reads the declaration and returns a *context-adapted* result (e.g. "which setup questions for this collection?") — without changing the schema. That's additive and opt-in; see [agent-tools.md](agent-tools.md).
650
-
651
- ## Reading the Files at Runtime
652
-
653
- ### Manifest — available from the widgets endpoint
654
-
655
- ```typescript
656
- import { appObjects } from '@proveanything/smartlinks';
657
- import type { AppManifest, AppAdminConfig } from '@proveanything/smartlinks';
658
-
659
- // The manifest arrives inline in the widgets response
660
- const { apps } = await SL.collection.getWidgets(collectionId);
661
- const { manifest, widget, container, admin: adminUrl } = apps[0];
662
-
663
- // manifest.meta.name, manifest.linkable, manifest.widgets.components, …
664
- ```
665
-
666
- ### Admin config — fetch separately, only when needed
667
-
668
- ```typescript
669
- // adminUrl is the fully-resolved URL from CollectionAppWidget.admin
670
- if (adminUrl) {
671
- const adminConfig: AppAdminConfig = await fetch(adminUrl).then(r => r.json());
672
- // adminConfig.setup.questions, adminConfig.tunable.fields, …
673
- }
674
- ```
675
-
676
- ---
677
-
678
- ## TypeScript Types
679
-
680
- ```typescript
681
- import type {
682
- AppManifest, // app.manifest.json
683
- AppAdminConfig, // app.admin.json
684
- DeepLinkEntry, // one entry in manifest.linkable or appConfig.linkable
685
- AppWidgetComponent, // one entry in manifest.widgets.components
686
- AppContainerComponent,
687
- AppManifestExecutor, // executor block in app.manifest.json
688
- AppBundle, // { js, css, source?, styles? }
689
- AppManifestFiles, // { js: { umd, esm? }, css? }
690
- CollectionAppWidget, // one app in the /widgets response
691
- CollectionWidgetsResponse,
692
- // Executor types
693
- ExecutorContext, // { collectionId, appId, SL }
694
- SEOInput,
695
- SEOResult,
696
- LLMContentInput,
697
- LLMContentResult,
698
- LLMContentSection,
699
- } from '@proveanything/smartlinks';
700
- ```
701
-
702
- All types live in `src/types/appManifest.ts`.
1
+ # App Configuration Files: `app.manifest.json` & `app.admin.json`
2
+
3
+ Every SmartLinks app ships with two JSON configuration files that the platform reads to understand what the app is and how to configure it. They have clearly separated responsibilities:
4
+
5
+ | File | Role | Loaded by |
6
+ |------|------|-----------|
7
+ | `app.manifest.json` | **Definitional** — what the app *is*: its bundles, components, static routes | Platform on every page load; portals; AI orchestrators |
8
+ | `app.admin.json` | **Operational** — how to *set up and tune* the app: setup questions, import schemas, tunable fields, metrics | Admin UI, AI-assisted setup flows |
9
+
10
+ The manifest always references the admin config via its `admin` field. Consumers that only need to *render* the app work entirely from the manifest. Only admin/setup flows need to fetch `app.admin.json`.
11
+
12
+ ```text
13
+ ┌─────────────────────────────────────────────────────────────────┐
14
+ │ Platform boot sequence │
15
+ │ │
16
+ │ 1. GET /collection/:id/widgets │
17
+ │ └─→ CollectionWidgetsResponse { apps: [...] } │
18
+ │ each app has: manifest, widget bundle, container │
19
+ │ │
20
+ │ 2. manifest.admin ──→ "app.admin.json" (pointer only) │
21
+ │ │
22
+ │ 3. Admin UI fetches app.admin.json when setup/config needed │
23
+ └─────────────────────────────────────────────────────────────────┘
24
+ ```
25
+
26
+ ---
27
+
28
+ ## `app.manifest.json`
29
+
30
+ The manifest is loaded automatically by the platform for every collection page. Keep it lean — it is fetched on every widget render.
31
+
32
+ ### Full Schema
33
+
34
+ ```json
35
+ {
36
+ "$schema": "https://smartlinks.app/schemas/app-manifest-v1.json",
37
+
38
+ "meta": {
39
+ "appId": "my-app",
40
+ "name": "My App",
41
+ "description": "A short human-readable description of what this app does.",
42
+ "version": "1.2.0",
43
+ "platformRevision": "R5",
44
+ "moduleFormat": "dual",
45
+ "sharedDependencies": "v7"
46
+ },
47
+
48
+ "admin": "app.admin.json",
49
+
50
+ "widgets": {
51
+ "instanceResolution": true,
52
+ "instanceParam": "widgetId",
53
+ "files": {
54
+ "js": {
55
+ "umd": "dist/widgets.umd.js",
56
+ "esm": "dist/widgets.esm.js"
57
+ },
58
+ "css": "dist/widgets.css"
59
+ },
60
+ "components": [
61
+ {
62
+ "name": "SummaryWidget",
63
+ "description": "Compact summary card for use on product pages.",
64
+ "sizes": ["compact", "standard"],
65
+ "props": {
66
+ "required": ["collectionId", "appId"],
67
+ "optional": ["productId", "proofId"]
68
+ },
69
+ "settings": {
70
+ "showImage": { "type": "boolean", "default": true }
71
+ }
72
+ }
73
+ ]
74
+ },
75
+
76
+ "containers": {
77
+ "files": {
78
+ "js": {
79
+ "umd": "dist/containers.umd.js",
80
+ "esm": "dist/containers.esm.js"
81
+ },
82
+ "css": "dist/containers.css"
83
+ },
84
+ "components": [
85
+ {
86
+ "name": "FullApp",
87
+ "description": "Full public app experience with internal routing.",
88
+ "props": {
89
+ "required": ["collectionId", "appId"],
90
+ "optional": ["productId", "proofId", "className"]
91
+ }
92
+ }
93
+ ]
94
+ },
95
+
96
+ "mobileAdmin": {
97
+ "files": {
98
+ "js": {
99
+ "umd": "dist/mobile-admin.umd.js",
100
+ "esm": "dist/mobile-admin.esm.js"
101
+ },
102
+ "css": null
103
+ },
104
+ "components": [
105
+ {
106
+ "name": "WarehousePickContainer",
107
+ "description": "In-field operator admin surface.",
108
+ "capabilities": ["nfc", "qr"],
109
+ "offline": true
110
+ }
111
+ ]
112
+ },
113
+
114
+ "linkable": [
115
+ { "title": "Home", "path": "/" },
116
+ { "title": "Gallery", "path": "/gallery" },
117
+ { "title": "Settings", "path": "/settings", "params": { "tab": "advanced" } }
118
+ ],
119
+
120
+ "records": {
121
+ "nutrition": {
122
+ "label": "Nutrition info",
123
+ "cardinality": "singleton",
124
+ "allowFacetRules": true,
125
+ "scopes": ["collection", "rule", "product", "facet", "batch"],
126
+ "defaultScope": "product"
127
+ },
128
+ "cooking_steps": {
129
+ "label": "Cooking steps",
130
+ "cardinality": "singleton",
131
+ "allowFacetRules": false,
132
+ "scopes": ["collection", "product"],
133
+ "defaultScope": "product"
134
+ }
135
+ }
136
+ }
137
+ ```
138
+
139
+ ### Field Reference
140
+
141
+ #### `meta`
142
+
143
+ | Field | Type | Required | Description |
144
+ |-------|------|----------|-------------|
145
+ | `appId` | string | ✅ | Unique identifier for the app (slug-style, e.g. `"warranty-tracker"`) |
146
+ | `name` | string | ✅ | Human-readable display name |
147
+ | `description` | string | ❌ | Short description shown in app directories and AI context |
148
+ | `version` | string | ✅ | SemVer string, e.g. `"1.2.0"` |
149
+ | `platformRevision` | string | ❌ | Platform revision tag this build targets, e.g. `"R5"` (see [host-dependency-contract.md](host-dependency-contract.md)) |
150
+ | `moduleFormat` | `"umd"` \| `"esm"` \| `"dual"` | ❌ | How the host loads this app's bundles. Absent = `"umd"`. See [Module format](#module-format-umd-vs-esm) below. |
151
+ | `sharedDependencies` | string | ❌ | Shared-dependency contract version the bundle was built against, e.g. `"v7"`. Used by the host to pick a compatible ESM import map. |
152
+ | `globals` | object | ❌ | Per-app namespaced UMD globals (R4.7+), e.g. `{ "widgets": "MyAppWidgets" }`. UMD-only; ESM bundles don't need it. |
153
+ | `seo.priority` | number | ❌ | Controls which app's `title`/`description`/`ogImage` wins when multiple apps are on the same page. Default `0`; higher wins. See the [Executor guide](executor.md). |
154
+
155
+ #### Module format (UMD vs ESM)
156
+
157
+ The host provides shared libraries (React, Radix, the SmartLinks SDK, LiquidJS…) as **singletons**
158
+ so apps never bundle their own. Two delivery mechanisms exist, and `meta.moduleFormat` tells the host
159
+ which to use:
160
+
161
+ | `moduleFormat` | Host behaviour |
162
+ |---|---|
163
+ | `"umd"` *(default)* | Loads `files.js.umd` via the CommonJS `require` shim; shared deps resolve from **window globals**. Every existing app works unchanged. |
164
+ | `"dual"` | Prefers `files.js.esm` when the host has an **import map** for the declared `sharedDependencies` version; **falls back to UMD** otherwise. The safe transition setting. |
165
+ | `"esm"` | Loads `files.js.esm` **natively**; if the host has no matching import map it fails with an actionable error rather than a bare-specifier crash. Use only once you know your hosts are on the contract. |
166
+
167
+ The ESM bundle is declared in the **same** `files.js` block as `esm` (there is no separate `jsEsm`
168
+ field):
169
+
170
+ ```jsonc
171
+ "widgets": {
172
+ "files": {
173
+ "js": { "umd": "dist/widgets.umd.js", "esm": "dist/widgets.esm.js" },
174
+ "css": "dist/widgets.css"
175
+ }
176
+ }
177
+ ```
178
+
179
+ An ESM bundle **must externalize exactly the shared-dependency contract** — read it from the SDK
180
+ (`SHARED_DEPENDENCY_SPECIFIERS`) rather than hard-coding it, and stamp the version you built against
181
+ into `meta.sharedDependencies`. See [host-dependency-contract.md](host-dependency-contract.md).
182
+
183
+ ##### Validate before you ship: `smartlinks doctor`
184
+
185
+ Run the checker (shipped with the SDK) against your built app — it reads the same contract the host
186
+ serves, so the two can't drift:
187
+
188
+ ```bash
189
+ npx smartlinks-doctor # in the app dir, after building
190
+ ```
191
+
192
+ It reads your manifest, and for every ESM surface confirms **every bare import in the bundle is a
193
+ contract entry**. A correctly-externalized ESM bundle inlines everything except the host singletons,
194
+ so anything else left as a bare import will either fail to resolve through the import map or silently
195
+ double-load (the duplicate-React class of bug). It also warns when a UMD app still declares stale
196
+ `*.es.js`/`*.esm.js` bundles the host will never load. Exit code is non-zero on violations, so it
197
+ drops straight into CI.
198
+
199
+ #### `build`
200
+
201
+ Optional build provenance. Recommended for the **Lovable dev publish** flow: stamp the content
202
+ hash your build already produces here so the platform can tell when a new version is live before it
203
+ registers (see [Deploying & registering](deploying-apps.md#a-from-lovable--hit-publish-no-key-anywhere-recommended-for-lovable-apps)).
204
+
205
+ | Field | Type | Description |
206
+ |-------|------|-------------|
207
+ | `build.hash` | string | Unique build/content hash — the platform waits for this to appear before registering, and uses it as the dev release version |
208
+ | `build.at` | string | ISO timestamp of the build (optional) |
209
+
210
+ ```json
211
+ "build": { "hash": "a1b2c3d4", "at": "2026-09-20T10:00:00Z" }
212
+ ```
213
+
214
+ #### `admin`
215
+
216
+ A relative path (from the app's public root) to the `app.admin.json` file. Omit entirely if the app has no admin UI.
217
+
218
+ ```json
219
+ "admin": "app.admin.json"
220
+ ```
221
+
222
+ #### `widgets`
223
+
224
+ Declares the widget bundle. Omit if the app has no widget component.
225
+
226
+ | Field | Description |
227
+ |-------|-------------|
228
+ | `files.js.umd` | UMD bundle path — used for dynamic `<script>` loading |
229
+ | `files.js.esm` | ESM bundle path — used for `import()` / native ES modules (optional but recommended) |
230
+ | `files.css` | CSS bundle path — omit if the widget ships no styles |
231
+ | `instanceResolution` | Optional boolean. When `true`, this app supports resolving configured widget instances by ID from app config |
232
+ | `instanceParam` | Optional string. Query/hash param used for instance lookup. Defaults to `"widgetId"` |
233
+ | `components[]` | One entry per exported widget component (see below) |
234
+
235
+ **Widget instance resolution**
236
+
237
+ Apps such as widget toolkits often store reusable widget instances in collection-scoped app config, for example under `config.widgets.launch-countdown`. When your widget bundle can self-configure from one of those stored instances, declare that capability in the manifest:
238
+
239
+ ```json
240
+ "widgets": {
241
+ "instanceResolution": true,
242
+ "instanceParam": "widgetId",
243
+ "files": {
244
+ "js": {
245
+ "umd": "dist/widgets.umd.js",
246
+ "esm": "dist/widgets.esm.js"
247
+ },
248
+ "css": null
249
+ },
250
+ "components": [
251
+ {
252
+ "name": "WidgetToolkitResolver",
253
+ "description": "Resolves and renders a configured widget instance by ID."
254
+ }
255
+ ]
256
+ }
257
+ ```
258
+
259
+ This tells the platform and other apps that they can deep-link into a stored widget instance using a URL or embed context such as `?appId=widget-toolkit&widgetId=launch-countdown`.
260
+
261
+ **Component fields:**
262
+
263
+ | Field | Type | Description |
264
+ |-------|------|-------------|
265
+ | `name` | string | Exported component name (must match the bundle export) |
266
+ | `description` | string | Human-readable description for portals and AI |
267
+ | `sizes` | string[] | Supported size hints: `"compact"`, `"standard"`, `"large"` |
268
+ | `props.required` | string[] | Props that must be provided for the component to render |
269
+ | `props.optional` | string[] | Props the component can use if provided |
270
+ | `settings` | object | JSON-Schema-style settings the widget accepts from its host |
271
+
272
+ #### `containers`
273
+
274
+ Same structure as `widgets` but declares the full-app container bundle. Lazy-loaded on demand.
275
+
276
+ See the [Containers guide](containers.md) for details on the container component model.
277
+ **Component fields** (same as widgets, plus):
278
+
279
+ | Field | Type | Description |
280
+ |-------|------|--------------|
281
+ | `name` | string | Exported component name |
282
+ | `description` | string | Human-readable description |
283
+ | `props.required` / `props.optional` | string[] | Required and optional prop names |
284
+ | `audience` | `"public"` \| `"admin"` \| `"both"` | Who can use/see this component. Defaults to `"public"`. |
285
+ | `scope` | `"collection"` \| `"product"` | Data scope hint. `"product"` means the component always renders in the context of a specific product. |
286
+ | `settings` | object | JSON Schema describing configurable settings |
287
+
288
+ #### `mobileAdmin`
289
+
290
+ Declares a **separate** mobile admin bundle — a sibling of `containers` with its own build output. Use this when the mobile admin surface needs a different runtime, native-only dependencies (Capacitor), or independent versioning. Omit if your app has no mobile admin surface.
291
+
292
+ See [mobile-admin-container.md](mobile-admin-container.md) for the `AdminMobileHostContext` prop contract, the capability matrix, event stream, error types, and build setup.
293
+
294
+ ```json
295
+ "mobileAdmin": {
296
+ "files": {
297
+ "js": {
298
+ "umd": "dist/mobile-admin.umd.js",
299
+ "esm": "dist/mobile-admin.esm.js"
300
+ },
301
+ "css": null
302
+ },
303
+ "components": [
304
+ {
305
+ "name": "WarehousePickContainer",
306
+ "description": "Pick orders by scanning NFC tags",
307
+ "capabilities": ["nfc", "qr"],
308
+ "offline": true
309
+ }
310
+ ]
311
+ }
312
+ ```
313
+
314
+ | Field | Description |
315
+ |-------|-------------|
316
+ | `files.js.umd` | UMD bundle path — used for dynamic `<script>` loading |
317
+ | `files.js.esm` | ESM bundle path (optional but recommended) |
318
+ | `files.css` | CSS bundle path — set to `null` if no styles |
319
+ | `components[].name` | Exported component name (must match the UMD bundle export) |
320
+ | `components[].description` | Shown in the mobile launcher's app picker |
321
+ | `components[].capabilities` | Hardware capabilities this component needs or can use. See [capability list](mobile-admin-container.md#hardware-capabilities--the-capability-matrix). |
322
+ | `components[].offline` | Set to `true` if this component queues writes locally and needs offline sync support. |
323
+ #### `linkable`
324
+
325
+ Static deep-linkable states built into the app — fixed routes that exist regardless of per-collection content. Declared once at build time.
326
+
327
+ See the [Deep Link Discovery guide](deep-link-discovery.md) for the full dual-source pattern (static manifest routes + dynamic `appConfig.linkable`).
328
+
329
+ | Field | Type | Required | Description |
330
+ |-------|------|----------|-------------|
331
+ | `title` | string | ✅ | Human-readable label shown in menus and offered to AI agents |
332
+ | `path` | string | ❌ | Hash route within the app (defaults to `"/"` if omitted) |
333
+ | `params` | object | ❌ | App-specific query params appended to the URL — do **not** include platform params (`collectionId`, `productId`, etc.) |
334
+
335
+ #### `records`
336
+
337
+ Declares which `app.records` record types the app stores, and which scopes each type supports. Required for any app that follows the [App Records Pattern](app-records-pattern.md). Omit if the app does not use scoped records.
338
+
339
+ The platform and the `<RecordsAdminShell>` from `@proveanything/smartlinks-utils-ui` read this block to render the right scope tabs, rule editor, and cardinality-appropriate right pane.
340
+
341
+ ```json
342
+ "records": {
343
+ "<recordType>": {
344
+ "label": "Human-readable label",
345
+ "cardinality": "singleton",
346
+ "allowFacetRules": false,
347
+ "scopes": ["collection", "product", "variant", "batch", "facet"],
348
+ "defaultScope": "product"
349
+ }
350
+ }
351
+ ```
352
+
353
+ | Field | Type | Default | Description |
354
+ |-------------------|----------|---------|-------------|
355
+ | `label` | string | — | Human-readable label for the record type, used in headings and tabs. |
356
+ | `cardinality` | string | `'singleton'` | `'singleton'` — one record wins per scope (e.g. ingredients, nutrition). `'collection'` — every matching record is returned in resolution order (e.g. FAQs, recipes). Drives which hook to use on the public side (`useResolvedRecord` vs `useCollectedRecords`) and how the shell lays out the right pane. |
357
+ | `allowFacetRules` | boolean | `false` | When `true`, the shell renders a **Rule** scope tab and embeds `<FacetRuleEditor>`. Add `'rule'` to `scopes` when setting this. |
358
+ | `scopes` | string[] | — | Allowed scope kinds in resolution order. Valid values: `"collection"`, `"product"`, `"variant"`, `"batch"`, `"facet"`, `"proof"`, `"rule"`. `'rule'` is a synthetic scope holding `facetRule`-targeted records. `'collection'` replaces the legacy empty-ref catch-all — **there is no `'global'` scope**. |
359
+ | `defaultScope` | string | — | The scope the "Create new" button targets in the admin shell. Must be one of the declared `scopes`. |
360
+
361
+ An app may declare multiple record types under different keys (e.g. `"nutrition"` and `"cooking_steps"`). See [app-records-pattern.md](app-records-pattern.md) for the full admin + public pattern.
362
+
363
+ #### `executor`
364
+
365
+ Declares the executor bundle — a standalone JS library for programmatic configuration, server-side SEO, and LLM content generation. Omit if the app has no executor.
366
+
367
+ See the **[Executor Model guide](executor.md)** for the full build setup, SEO contract, LLM content contract, and implementation patterns.
368
+
369
+ | Field | Type | Description |
370
+ |-------|------|-------------|
371
+ | `files.js.umd` | string | UMD bundle path |
372
+ | `files.js.esm` | string | ESM bundle path |
373
+ | `factory` | string | Name of the factory function that creates an executor instance |
374
+ | `exports` | string[] | All named exports — tells consumers what's available without loading the bundle |
375
+ | `description` | string | Human-readable summary for AI orchestrators |
376
+ | `llmContent.function` | string | Name of the `getLLMContent` export |
377
+ | `llmContent.timeout` | number | Timeout in ms (default 500) |
378
+
379
+ #### `functions`
380
+
381
+ Declares app-authored **server functions** — server-side handlers `(ctx, event) ⇒ result`
382
+ (http / event / cron), shipped in a self-contained UMD bundle. Omit if the app has no server
383
+ functions. The security model (`visibility`/`authority`/`capabilities`), the runtime surface, how
384
+ to build the bundle, and how to invoke are all in the **[Server Functions guide](server-functions.md)** —
385
+ this is just the manifest shape.
386
+
387
+ | Field | Type | Description |
388
+ |-------|------|-------------|
389
+ | `files.js.umd` | string | UMD bundle path (e.g. `dist/functions.umd.js`) — a self-contained bundle exporting each handler by name |
390
+ | `definitions[]` | object[] | One entry per function |
391
+ | `definitions[].name` | string | Function name; also the handler export name (override with `handler`) |
392
+ | `definitions[].trigger` | object | `{ type: "http"|"event"|"cron", … }` |
393
+ | `definitions[].visibility` | string | `admin` \| `public` (http only) — who may call it |
394
+ | `definitions[].authority` | string | `caller` \| `collection` — whose authority it runs as |
395
+ | `definitions[].capabilities` | string[] | Least-privilege grants (`sl:<res>:<read\|write>`, `network[:<host>]`, `secrets:<ref>`) |
396
+
397
+ ---
398
+
399
+ ## `app.admin.json`
400
+
401
+ Fetched only by the admin UI and AI-assisted setup flows — never loaded on the public-facing page. Keep setup logic and configuration schemas here, not in the manifest.
402
+
403
+ ### Full Schema
404
+
405
+ ```json
406
+ {
407
+ "$schema": "https://smartlinks.app/schemas/app-admin-v1.json",
408
+
409
+ "aiGuide": "ai-guide.md",
410
+
411
+ "setup": {
412
+ "description": "Configure the app for this collection.",
413
+ "questions": [
414
+ {
415
+ "id": "brandName",
416
+ "prompt": "What is your brand name?",
417
+ "type": "text",
418
+ "required": true
419
+ },
420
+ {
421
+ "id": "primaryColor",
422
+ "prompt": "Choose a primary theme colour.",
423
+ "type": "select",
424
+ "options": [
425
+ { "value": "blue", "label": "Blue" },
426
+ { "value": "green", "label": "Green" },
427
+ { "value": "red", "label": "Red" }
428
+ ]
429
+ },
430
+ {
431
+ "id": "welcomeEnabled",
432
+ "prompt": "Show a welcome message to first-time visitors?",
433
+ "type": "boolean",
434
+ "default": true
435
+ }
436
+ ],
437
+ "configSchema": {
438
+ "brandName": { "type": "string" },
439
+ "primaryColor": { "type": "string" },
440
+ "welcomeEnabled": { "type": "boolean" }
441
+ },
442
+ "saveWith": {
443
+ "method": "appConfiguration.setConfig",
444
+ "scope": "collection",
445
+ "admin": true,
446
+ "note": "Saved under the collection scope; readable by all app users."
447
+ },
448
+ "contentHints": {
449
+ "welcomeMessage": {
450
+ "aiGenerate": true,
451
+ "prompt": "Write a short, friendly welcome message for a brand called {{brandName}}."
452
+ }
453
+ }
454
+ },
455
+
456
+ "import": {
457
+ "description": "Bulk-import items via CSV.",
458
+ "scope": "collection",
459
+ "fields": [
460
+ { "name": "title", "type": "string", "required": true },
461
+ { "name": "description", "type": "string" },
462
+ { "name": "imageUrl", "type": "string" },
463
+ { "name": "price", "type": "number", "default": 0 }
464
+ ],
465
+ "csvExample": "title,description,imageUrl,price\nWidget A,Our first widget,https://example.com/img.jpg,9.99",
466
+ "saveWith": {
467
+ "method": "appObjects.createRecord",
468
+ "scope": "collection",
469
+ "admin": true
470
+ }
471
+ },
472
+
473
+ "tunable": {
474
+ "description": "Adjust display options after initial setup.",
475
+ "fields": [
476
+ {
477
+ "name": "displayMode",
478
+ "description": "How items are laid out on the page.",
479
+ "type": "select",
480
+ "options": ["grid", "list", "carousel"]
481
+ },
482
+ {
483
+ "name": "itemsPerPage",
484
+ "description": "Number of items shown per page.",
485
+ "type": "number"
486
+ }
487
+ ]
488
+ },
489
+
490
+ "metrics": {
491
+ "interactions": [
492
+ { "id": "view", "description": "User viewed an item." },
493
+ { "id": "click", "description": "User clicked a link or CTA." },
494
+ { "id": "purchase", "description": "User completed a purchase." }
495
+ ],
496
+ "kpis": [
497
+ { "name": "Click-through Rate", "compute": "click / view" },
498
+ { "name": "Conversion Rate", "compute": "purchase / view" }
499
+ ]
500
+ }
501
+ }
502
+ ```
503
+
504
+ > `dynamic-select` widget pickers are a reasonable future extension for admin schemas, but they are not a built-in question type in the SDK today. For now, treat widget-instance selection as an app-level UI convention powered by `appConfiguration.listWidgetInstances()`.
505
+
506
+ ### Field Reference
507
+
508
+ #### `aiGuide`
509
+
510
+ Path (relative to the app's public root) to a Markdown file providing natural-language context for AI-assisted configuration. See the [AI Guide Template](ai-guide-template.md).
511
+
512
+ ```json
513
+ "aiGuide": "ai-guide.md"
514
+ ```
515
+
516
+ ---
517
+
518
+ #### `setup`
519
+
520
+ Drives the initial configuration wizard shown to admins when they first install the app for a collection.
521
+
522
+ | Field | Type | Description |
523
+ |-------|------|-------------|
524
+ | `description` | string | Intro text shown at the top of the setup wizard |
525
+ | `questions` | array | Ordered list of questions to ask the admin (see below) |
526
+ | `configSchema` | object | JSON-Schema-style shape of the resulting config object |
527
+ | `saveWith` | object | Which SDK method and scope to use when persisting answers |
528
+ | `contentHints` | object | Keys that AI should auto-generate based on question answers |
529
+
530
+ **`questions[]` fields:**
531
+
532
+ | Field | Type | Required | Description |
533
+ |-------|------|----------|-------------|
534
+ | `id` | string | ✅ | Key used in the saved config and in `contentHints` references |
535
+ | `prompt` | string | ✅ | Question text displayed to the admin |
536
+ | `type` | string | ✅ | Input type: `"text"`, `"number"`, `"boolean"`, `"select"`, `"multiselect"`, `"textarea"` |
537
+ | `required` | boolean | ❌ | Whether an answer is mandatory (default `false`) |
538
+ | `default` | any | ❌ | Pre-filled default value |
539
+ | `options` | array | ❌ | For `select`/`multiselect`: `[{ "value": "...", "label": "..." }]` |
540
+
541
+ **`saveWith` fields:**
542
+
543
+ | Field | Type | Description |
544
+ |-------|------|-------------|
545
+ | `method` | string | SDK method to call, e.g. `"appConfiguration.setConfig"` |
546
+ | `scope` | string | Data scope: `"collection"`, `"product"`, or `"proof"` |
547
+ | `admin` | boolean | Whether the save call requires admin credentials |
548
+ | `note` | string | Human-readable note explaining the save behaviour |
549
+
550
+ **`contentHints`:**
551
+
552
+ A map from content key to AI generation instructions. The AI setup flow uses this to pre-fill content fields after the admin answers setup questions.
553
+
554
+ ```json
555
+ "contentHints": {
556
+ "welcomeMessage": {
557
+ "aiGenerate": true,
558
+ "prompt": "Write a short welcome message for a brand called {{brandName}}."
559
+ }
560
+ }
561
+ ```
562
+
563
+ ---
564
+
565
+ #### `import`
566
+
567
+ Defines a CSV bulk-import flow available in the admin UI.
568
+
569
+ | Field | Type | Description |
570
+ |-------|------|-------------|
571
+ | `description` | string | Explains what will be imported and how |
572
+ | `scope` | string | Data scope for the imported records |
573
+ | `fields` | array | Column definitions — `name`, `type`, `required`, `default`, `description` |
574
+ | `csvExample` | string | A sample CSV string (shown as a download template) |
575
+ | `saveWith` | object | SDK method used to persist each imported row (same shape as `setup.saveWith`) |
576
+
577
+ ---
578
+
579
+ #### `tunable`
580
+
581
+ Post-setup display options that admins can tweak without re-running the full setup wizard.
582
+
583
+ | Field | Type | Description |
584
+ |-------|------|-------------|
585
+ | `description` | string | Explains what these settings control |
586
+ | `fields` | array | Tunable parameters — `name`, `description`, `type`, `options[]` |
587
+
588
+ ---
589
+
590
+ #### `metrics`
591
+
592
+ Declares what interactions and KPIs the app reports. Used by the platform's analytics dashboard.
593
+
594
+ | Field | Type | Description |
595
+ |-------|------|-------------|
596
+ | `interactions` | array | Interaction event types: `{ id, description }` |
597
+ | `kpis` | array | Derived metrics: `{ name, compute }` — `compute` is a simple expression over interaction IDs |
598
+
599
+ ---
600
+
601
+ ## Widget settings schema (JSON Schema)
602
+
603
+ Each widget component's `settings` object uses **JSON Schema** to describe its configurable props, so schema-form renderers *and* AI orchestrators can auto-generate a config UI without per-widget code:
604
+
605
+ ```json
606
+ "components": [
607
+ {
608
+ "name": "MyWidget",
609
+ "description": "What this widget does",
610
+ "sizes": ["compact", "standard", "large"],
611
+ "settings": {
612
+ "type": "object",
613
+ "properties": {
614
+ "displayMode": {
615
+ "type": "string", "title": "Display Mode",
616
+ "description": "How the widget renders",
617
+ "enum": ["compact", "standard", "large"],
618
+ "enumLabels": { "compact": "Icons only", "standard": "With names", "large": "Full cards" },
619
+ "default": "standard", "order": 1
620
+ },
621
+ "showImage": { "type": "boolean", "title": "Show Product Image", "default": true, "order": 2 }
622
+ }
623
+ }
624
+ }
625
+ ]
626
+ ```
627
+
628
+ | Field | Purpose |
629
+ |-------|---------|
630
+ | `type`, `enum` | Standard JSON Schema validation |
631
+ | `title` | Human-readable form label |
632
+ | `description` | Help text shown alongside the field |
633
+ | `enumLabels` | Friendly display names for enum values (`{ value → label }`) |
634
+ | `default` | Pre-selected value when no config exists |
635
+ | `order` | Field display order (lower number = higher) |
636
+
637
+ The schema serves both AI orchestrators (understand what a widget accepts, configure it conversationally) and schema-form renderers (auto-generate settings UIs).
638
+
639
+ ## AI workflows the manifest enables
640
+
641
+ SmartLinks manifests are **AI-discoverable, -configurable, and -importable**: the structured manifest plus the prose `ai-guide.md` (start from [ai-guide-template.md](ai-guide-template.md)) let AI systems set up and populate apps without custom integration code. Three workflows read the manifest:
642
+
643
+ 1. **Widget Builder** — reads `widgets.components[]` and each `settings` schema to embed a widget with correct props and auto-render its configuration UI.
644
+ 2. **Setup Wizard** — reads `setup` from `app.admin.json`: walks `setup.questions[]`, validates against `setup.configSchema`, optionally auto-generates content via `SL.ai`, then saves per `setup.saveWith`.
645
+ 3. **Data Importer** — reads `import` from `app.admin.json`: builds a CSV template from `import.fields[]`, normalises rows, and calls `import.saveWith.method` per row (multi-app imports merge fields into one CSV).
646
+
647
+ When you change your config shape, keep all three in sync: `app.manifest.json` (widget `settings`, containers, executor, linkable), `app.admin.json` (setup / import / tunable), and `ai-guide.md` (prose guidance).
648
+
649
+ This declarative model is canonical and works today. An app may **optionally** layer agent tools on top — a function that reads the declaration and returns a *context-adapted* result (e.g. "which setup questions for this collection?") — without changing the schema. That's additive and opt-in; see [agent-tools.md](agent-tools.md).
650
+
651
+ ## Reading the Files at Runtime
652
+
653
+ ### Manifest — available from the widgets endpoint
654
+
655
+ ```typescript
656
+ import { appObjects } from '@proveanything/smartlinks';
657
+ import type { AppManifest, AppAdminConfig } from '@proveanything/smartlinks';
658
+
659
+ // The manifest arrives inline in the widgets response
660
+ const { apps } = await SL.collection.getWidgets(collectionId);
661
+ const { manifest, widget, container, admin: adminUrl } = apps[0];
662
+
663
+ // manifest.meta.name, manifest.linkable, manifest.widgets.components, …
664
+ ```
665
+
666
+ ### Admin config — fetch separately, only when needed
667
+
668
+ ```typescript
669
+ // adminUrl is the fully-resolved URL from CollectionAppWidget.admin
670
+ if (adminUrl) {
671
+ const adminConfig: AppAdminConfig = await fetch(adminUrl).then(r => r.json());
672
+ // adminConfig.setup.questions, adminConfig.tunable.fields, …
673
+ }
674
+ ```
675
+
676
+ ---
677
+
678
+ ## TypeScript Types
679
+
680
+ ```typescript
681
+ import type {
682
+ AppManifest, // app.manifest.json
683
+ AppAdminConfig, // app.admin.json
684
+ DeepLinkEntry, // one entry in manifest.linkable or appConfig.linkable
685
+ AppWidgetComponent, // one entry in manifest.widgets.components
686
+ AppContainerComponent,
687
+ AppManifestExecutor, // executor block in app.manifest.json
688
+ AppBundle, // { js, css, source?, styles? }
689
+ AppManifestFiles, // { js: { umd, esm? }, css? }
690
+ CollectionAppWidget, // one app in the /widgets response
691
+ CollectionWidgetsResponse,
692
+ // Executor types
693
+ ExecutorContext, // { collectionId, appId, SL }
694
+ SEOInput,
695
+ SEOResult,
696
+ LLMContentInput,
697
+ LLMContentResult,
698
+ LLMContentSection,
699
+ } from '@proveanything/smartlinks';
700
+ ```
701
+
702
+ All types live in `src/types/appManifest.ts`.