Getting Started

    app.manifest.json & app.admin.json

    App configuration files: roles, responsibilities, and how the platform loads them.

    App Configuration Files: app.manifest.json & app.admin.json

    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:

    FileRoleLoaded by
    app.manifest.jsonDefinitional — what the app is: its bundles, components, static routesPlatform on every page load; portals; AI orchestrators
    app.admin.jsonOperational — how to set up and tune the app: setup questions, import schemas, tunable fields, metricsAdmin UI, AI-assisted setup flows

    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.

    ┌─────────────────────────────────────────────────────────────────┐
    │ Platform boot sequence                                          │
    │                                                                 │
    │  1. GET /collection/:id/widgets                                 │
    │       └─→ CollectionWidgetsResponse { apps: [...] }            │
    │              each app has: manifest, widget bundle, container   │
    │                                                                 │
    │  2. manifest.admin  ──→  "app.admin.json"  (pointer only)      │
    │                                                                 │
    │  3. Admin UI fetches app.admin.json when setup/config needed    │
    └─────────────────────────────────────────────────────────────────┘
    

    app.manifest.json

    The manifest is loaded automatically by the platform for every collection page. Keep it lean — it is fetched on every widget render.

    Full Schema

    {
      "$schema": "https://smartlinks.app/schemas/app-manifest-v1.json",
    
      "meta": {
        "appId": "my-app",
        "name": "My App",
        "description": "A short human-readable description of what this app does.",
        "version": "1.2.0",
        "platformRevision": "2026-01-01"
      },
    
      "admin": "app.admin.json",
    
      "widgets": {
        "instanceResolution": true,
        "instanceParam": "widgetId",
        "files": {
          "js": {
            "umd": "dist/widgets.umd.js",
            "esm": "dist/widgets.es.js"
          },
          "css": "dist/widgets.css"
        },
        "components": [
          {
            "name": "SummaryWidget",
            "description": "Compact summary card for use on product pages.",
            "sizes": ["compact", "standard"],
            "props": {
              "required": ["collectionId", "appId"],
              "optional": ["productId", "proofId"]
            },
            "settings": {
              "showImage": { "type": "boolean", "default": true }
            }
          }
        ]
      },
    
      "containers": {
        "files": {
          "js": {
            "umd": "dist/containers.umd.js",
            "esm": "dist/containers.es.js"
          },
          "css": "dist/containers.css"
        },
        "components": [
          {
            "name": "FullApp",
            "description": "Full public app experience with internal routing.",
            "props": {
              "required": ["collectionId", "appId"],
              "optional": ["productId", "proofId", "className"]
            }
          }
        ]
      },
    
      "mobileAdmin": {
        "files": {
          "js": {
            "umd": "dist/mobile-admin.umd.js",
            "esm": "dist/mobile-admin.es.js"
          },
          "css": null
        },
        "components": [
          {
            "name": "WarehousePickContainer",
            "description": "In-field operator admin surface.",
            "capabilities": ["nfc", "qr"],
            "offline": true
          }
        ]
      },
    
      "linkable": [
        { "title": "Home",     "path": "/" },
        { "title": "Gallery",  "path": "/gallery" },
        { "title": "Settings", "path": "/settings", "params": { "tab": "advanced" } }
      ],
    
      "records": {
        "nutrition": {
          "label": "Nutrition info",
          "cardinality": "singleton",
          "allowFacetRules": true,
          "scopes": ["collection", "rule", "product", "facet", "batch"],
          "defaultScope": "product"
        },
        "cooking_steps": {
          "label": "Cooking steps",
          "cardinality": "singleton",
          "allowFacetRules": false,
          "scopes": ["collection", "product"],
          "defaultScope": "product"
        }
      }
    }
    

    Field Reference

    meta

    FieldTypeRequiredDescription
    appIdstringUnique identifier for the app (slug-style, e.g. "warranty-tracker")
    namestringHuman-readable display name
    descriptionstringShort description shown in app directories and AI context
    versionstringSemVer string, e.g. "1.2.0"
    platformRevisionstringISO date string marking the platform API revision this build targets
    seo.prioritynumberControls which app's title/description/ogImage wins when multiple apps are on the same page. Default 0; higher wins. See the Executor guide.

    admin

    A relative path (from the app's public root) to the app.admin.json file. Omit entirely if the app has no admin UI.

    "admin": "app.admin.json"
    

    widgets

    Declares the widget bundle. Omit if the app has no widget component.

    FieldDescription
    files.js.umdUMD bundle path — used for dynamic <script> loading
    files.js.esmESM bundle path — used for import() / native ES modules (optional but recommended)
    files.cssCSS bundle path — omit if the widget ships no styles
    instanceResolutionOptional boolean. When true, this app supports resolving configured widget instances by ID from app config
    instanceParamOptional string. Query/hash param used for instance lookup. Defaults to "widgetId"
    components[]One entry per exported widget component (see below)

    Widget instance resolution

    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:

    "widgets": {
      "instanceResolution": true,
      "instanceParam": "widgetId",
      "files": {
        "js": {
          "umd": "dist/widgets.umd.js",
          "esm": "dist/widgets.es.js"
        },
        "css": null
      },
      "components": [
        {
          "name": "WidgetToolkitResolver",
          "description": "Resolves and renders a configured widget instance by ID."
        }
      ]
    }
    

    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.

    Component fields:

    FieldTypeDescription
    namestringExported component name (must match the bundle export)
    descriptionstringHuman-readable description for portals and AI
    sizesstring[]Supported size hints: "compact", "standard", "large"
    props.requiredstring[]Props that must be provided for the component to render
    props.optionalstring[]Props the component can use if provided
    settingsobjectJSON-Schema-style settings the widget accepts from its host

    containers

    Same structure as widgets but declares the full-app container bundle. Lazy-loaded on demand.

    See the Containers guide for details on the container component model. Component fields (same as widgets, plus):

    FieldTypeDescription
    namestringExported component name
    descriptionstringHuman-readable description
    props.required / props.optionalstring[]Required and optional prop names
    audience"public" | "admin" | "both"Who can use/see this component. Defaults to "public".
    scope"collection" | "product"Data scope hint. "product" means the component always renders in the context of a specific product.
    settingsobjectJSON Schema describing configurable settings

    mobileAdmin

    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.

    See mobile-admin-container.md for the AdminMobileHostContext prop contract, the capability matrix, event stream, error types, and build setup.

    "mobileAdmin": {
      "files": {
        "js": {
          "umd": "dist/mobile-admin.umd.js",
          "esm": "dist/mobile-admin.es.js"
        },
        "css": null
      },
      "components": [
        {
          "name": "WarehousePickContainer",
          "description": "Pick orders by scanning NFC tags",
          "capabilities": ["nfc", "qr"],
          "offline": true
        }
      ]
    }
    
    FieldDescription
    files.js.umdUMD bundle path — used for dynamic <script> loading
    files.js.esmESM bundle path (optional but recommended)
    files.cssCSS bundle path — set to null if no styles
    components[].nameExported component name (must match the UMD bundle export)
    components[].descriptionShown in the mobile launcher's app picker
    components[].capabilitiesHardware capabilities this component needs or can use. See capability list.
    components[].offlineSet to true if this component queues writes locally and needs offline sync support.

    linkable

    Static deep-linkable states built into the app — fixed routes that exist regardless of per-collection content. Declared once at build time.

    See the Deep Link Discovery guide for the full dual-source pattern (static manifest routes + dynamic appConfig.linkable).

    FieldTypeRequiredDescription
    titlestringHuman-readable label shown in menus and offered to AI agents
    pathstringHash route within the app (defaults to "/" if omitted)
    paramsobjectApp-specific query params appended to the URL — do not include platform params (collectionId, productId, etc.)

    records

    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. Omit if the app does not use scoped records.

    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.

    "records": {
      "<recordType>": {
        "label": "Human-readable label",
        "cardinality": "singleton",
        "allowFacetRules": false,
        "scopes": ["collection", "product", "variant", "batch", "facet"],
        "defaultScope": "product"
      }
    }
    
    FieldTypeDefaultDescription
    labelstringHuman-readable label for the record type, used in headings and tabs.
    cardinalitystring'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.
    allowFacetRulesbooleanfalseWhen true, the shell renders a Rule scope tab and embeds <FacetRuleEditor>. Add 'rule' to scopes when setting this.
    scopesstring[]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.
    defaultScopestringThe scope the "Create new" button targets in the admin shell. Must be one of the declared scopes.

    An app may declare multiple record types under different keys (e.g. "nutrition" and "cooking_steps"). See app-records-pattern.md for the full admin + public pattern.

    executor

    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.

    See the Executor Model guide for the full build setup, SEO contract, LLM content contract, and implementation patterns.

    FieldTypeDescription
    files.js.umdstringUMD bundle path
    files.js.esmstringESM bundle path
    factorystringName of the factory function that creates an executor instance
    exportsstring[]All named exports — tells consumers what's available without loading the bundle
    descriptionstringHuman-readable summary for AI orchestrators
    llmContent.functionstringName of the getLLMContent export
    llmContent.timeoutnumberTimeout in ms (default 500)

    app.admin.json

    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.

    Full Schema

    {
      "$schema": "https://smartlinks.app/schemas/app-admin-v1.json",
    
      "aiGuide": "ai-guide.md",
    
      "setup": {
        "description": "Configure the app for this collection.",
        "questions": [
          {
            "id": "brandName",
            "prompt": "What is your brand name?",
            "type": "text",
            "required": true
          },
          {
            "id": "primaryColor",
            "prompt": "Choose a primary theme colour.",
            "type": "select",
            "options": [
              { "value": "blue",  "label": "Blue" },
              { "value": "green", "label": "Green" },
              { "value": "red",   "label": "Red" }
            ]
          },
          {
            "id": "welcomeEnabled",
            "prompt": "Show a welcome message to first-time visitors?",
            "type": "boolean",
            "default": true
          }
        ],
        "configSchema": {
          "brandName":      { "type": "string" },
          "primaryColor":   { "type": "string" },
          "welcomeEnabled": { "type": "boolean" }
        },
        "saveWith": {
          "method": "appConfiguration.setConfig",
          "scope": "collection",
          "admin": true,
          "note": "Saved under the collection scope; readable by all app users."
        },
        "contentHints": {
          "welcomeMessage": {
            "aiGenerate": true,
            "prompt": "Write a short, friendly welcome message for a brand called {{brandName}}."
          }
        }
      },
    
      "import": {
        "description": "Bulk-import items via CSV.",
        "scope": "collection",
        "fields": [
          { "name": "title",       "type": "string",  "required": true  },
          { "name": "description", "type": "string"                     },
          { "name": "imageUrl",    "type": "string"                     },
          { "name": "price",       "type": "number",  "default": 0      }
        ],
        "csvExample": "title,description,imageUrl,price\nWidget A,Our first widget,https://example.com/img.jpg,9.99",
        "saveWith": {
          "method": "appObjects.createRecord",
          "scope": "collection",
          "admin": true
        }
      },
    
      "tunable": {
        "description": "Adjust display options after initial setup.",
        "fields": [
          {
            "name": "displayMode",
            "description": "How items are laid out on the page.",
            "type": "select",
            "options": ["grid", "list", "carousel"]
          },
          {
            "name": "itemsPerPage",
            "description": "Number of items shown per page.",
            "type": "number"
          }
        ]
      },
    
      "metrics": {
        "interactions": [
          { "id": "view",     "description": "User viewed an item." },
          { "id": "click",    "description": "User clicked a link or CTA." },
          { "id": "purchase", "description": "User completed a purchase." }
        ],
        "kpis": [
          { "name": "Click-through Rate", "compute": "click / view" },
          { "name": "Conversion Rate",    "compute": "purchase / view" }
        ]
      }
    }
    

    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().

    Field Reference

    aiGuide

    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.

    "aiGuide": "ai-guide.md"
    

    setup

    Drives the initial configuration wizard shown to admins when they first install the app for a collection.

    FieldTypeDescription
    descriptionstringIntro text shown at the top of the setup wizard
    questionsarrayOrdered list of questions to ask the admin (see below)
    configSchemaobjectJSON-Schema-style shape of the resulting config object
    saveWithobjectWhich SDK method and scope to use when persisting answers
    contentHintsobjectKeys that AI should auto-generate based on question answers

    questions[] fields:

    FieldTypeRequiredDescription
    idstringKey used in the saved config and in contentHints references
    promptstringQuestion text displayed to the admin
    typestringInput type: "text", "number", "boolean", "select", "multiselect", "textarea"
    requiredbooleanWhether an answer is mandatory (default false)
    defaultanyPre-filled default value
    optionsarrayFor select/multiselect: [{ "value": "...", "label": "..." }]

    saveWith fields:

    FieldTypeDescription
    methodstringSDK method to call, e.g. "appConfiguration.setConfig"
    scopestringData scope: "collection", "product", or "proof"
    adminbooleanWhether the save call requires admin credentials
    notestringHuman-readable note explaining the save behaviour

    contentHints:

    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.

    "contentHints": {
      "welcomeMessage": {
        "aiGenerate": true,
        "prompt": "Write a short welcome message for a brand called {{brandName}}."
      }
    }
    

    import

    Defines a CSV bulk-import flow available in the admin UI.

    FieldTypeDescription
    descriptionstringExplains what will be imported and how
    scopestringData scope for the imported records
    fieldsarrayColumn definitions — name, type, required, default, description
    csvExamplestringA sample CSV string (shown as a download template)
    saveWithobjectSDK method used to persist each imported row (same shape as setup.saveWith)

    tunable

    Post-setup display options that admins can tweak without re-running the full setup wizard.

    FieldTypeDescription
    descriptionstringExplains what these settings control
    fieldsarrayTunable parameters — name, description, type, options[]

    metrics

    Declares what interactions and KPIs the app reports. Used by the platform's analytics dashboard.

    FieldTypeDescription
    interactionsarrayInteraction event types: { id, description }
    kpisarrayDerived metrics: { name, compute }compute is a simple expression over interaction IDs

    Reading the Files at Runtime

    Manifest — available from the widgets endpoint

    import { appObjects } from '@proveanything/smartlinks';
    import type { AppManifest, AppAdminConfig } from '@proveanything/smartlinks';
    
    // The manifest arrives inline in the widgets response
    const { apps } = await SL.collection.getWidgets(collectionId);
    const { manifest, widget, container, admin: adminUrl } = apps[0];
    
    // manifest.meta.name, manifest.linkable, manifest.widgets.components, …
    

    Admin config — fetch separately, only when needed

    // adminUrl is the fully-resolved URL from CollectionAppWidget.admin
    if (adminUrl) {
      const adminConfig: AppAdminConfig = await fetch(adminUrl).then(r => r.json());
      // adminConfig.setup.questions, adminConfig.tunable.fields, …
    }
    

    TypeScript Types

    import type {
      AppManifest,           // app.manifest.json
      AppAdminConfig,        // app.admin.json
      DeepLinkEntry,         // one entry in manifest.linkable or appConfig.linkable
      AppWidgetComponent,    // one entry in manifest.widgets.components
      AppContainerComponent,
      AppManifestExecutor,   // executor block in app.manifest.json
      AppBundle,             // { js, css, source?, styles? }
      AppManifestFiles,      // { js: { umd, esm? }, css? }
      CollectionAppWidget,   // one app in the /widgets response
      CollectionWidgetsResponse,
      // Executor types
      ExecutorContext,       // { collectionId, appId, SL }
      SEOInput,
      SEOResult,
      LLMContentInput,
      LLMContentResult,
      LLMContentSection,
    } from '@proveanything/smartlinks';
    

    All types live in src/types/appManifest.ts.