{
    "$schema": "http://json-schema.org/draft-07/schema#",
    "$id": "https://manifestx.dev/manifest.schema.json",
    "title": "Manifest project configuration",
    "description": "Combined PWA manifest and Manifest framework configuration. See https://manifestx.dev/docs/getting-started/setup.",
    "type": "object",
    "additionalProperties": true,
    "properties": {
        "$schema": {
            "type": "string",
            "format": "uri"
        },
        "name": {
            "type": "string",
            "description": "Full application name shown by the OS and browsers."
        },
        "short_name": {
            "type": "string",
            "description": "Short name used on home screens and limited-width contexts."
        },
        "description": {
            "type": "string"
        },
        "author": {
            "type": "string"
        },
        "email": {
            "type": "string",
            "format": "email"
        },
        "live_url": {
            "type": "string",
            "format": "uri",
            "description": "Production URL — used by the prerender pipeline for canonical/sitemap output."
        },
        "start_url": {
            "type": "string",
            "default": "/"
        },
        "scope": {
            "type": "string",
            "default": "/"
        },
        "display": {
            "type": "string",
            "enum": ["fullscreen", "standalone", "minimal-ui", "browser"]
        },
        "orientation": {
            "type": "string",
            "enum": ["any", "natural", "landscape", "landscape-primary", "landscape-secondary", "portrait", "portrait-primary", "portrait-secondary"]
        },
        "background_color": {
            "type": "string",
            "pattern": "^#([0-9A-Fa-f]{3}){1,2}$"
        },
        "theme_color": {
            "type": "string",
            "pattern": "^#([0-9A-Fa-f]{3}){1,2}$"
        },
        "icons": {
            "type": "array",
            "items": {
                "type": "object",
                "required": ["src"],
                "properties": {
                    "src": { "type": "string" },
                    "sizes": { "type": "string" },
                    "type": { "type": "string" },
                    "purpose": { "type": "string" }
                }
            }
        },
        "components": {
            "type": "array",
            "description": "Component HTML files registered as <x-name> tags.",
            "items": { "type": "string" }
        },
        "version": {
            "type": ["string", "number"],
            "description": "Optional author-set release stamp. Appended as ?v= to component HTML fetches so browser caches bust on change. mnfst-publish injects a content-derived `deployment` stamp automatically, which takes precedence."
        },
        "deployment": {
            "type": "string",
            "description": "Publish-injected content hash of the component files (set by mnfst-publish; not hand-authored). Appended as ?v= to component HTML fetches."
        },
        "sw": {
            "type": "boolean",
            "description": "App-shell service worker kill switch. `false` stops the loader registering /sw.js and unregisters an installed worker on the next load. Omit (default) to let the loader infer: https origin, not local dev, /sw.js stub present."
        },
        "preloadedComponents": {
            "type": "array",
            "description": "Components loaded eagerly on initial page load (header, logo, etc.).",
            "items": { "type": "string" }
        },
        "publishIgnore": {
            "type": "array",
            "description": "Gitignore-style patterns for files to exclude from the published bundle, independent of git. Applied on top of .gitignore and the built-in secret guards. A .manifestignore file at the project root works the same way.",
            "items": { "type": "string" }
        },
        "assets": {
            "type": "array",
            "description": "Paths or simple globs (e.g. \"sprites/*.png\") of files the hosting service worker precaches for offline use even when nothing in the HTML references them (dynamically loaded sprites, audio, fonts). Files referenced from HTML/CSS are precached automatically.",
            "items": { "type": "string" }
        },
        "data": {
            "type": "object",
            "description": "Named data sources made available via $x.<sourceName>. Each value is either a string path/URL or a config object.",
            "additionalProperties": {
                "oneOf": [
                    {
                        "type": "string",
                        "description": "Path to a CSV, JSON, or YAML file, or an absolute HTTP URL."
                    },
                    {
                        "type": "object",
                        "description": "Localized source, HTTP source with config, or Appwrite-backed source.",
                        "properties": {
                            "url": {
                                "type": "string",
                                "description": "HTTP endpoint URL."
                            },
                            "headers": {
                                "type": "object",
                                "additionalProperties": { "type": "string" }
                            },
                            "params": {
                                "type": "object"
                            },
                            "transform": {
                                "type": "string",
                                "description": "Optional dot-path into the response body (e.g. 'data.items')."
                            },
                            "defaultValue": {
                                "description": "Value used while loading or on error."
                            },
                            "locales": {
                                "type": "string",
                                "description": "Single CSV with locale columns; locale resolution happens internally."
                            },
                            "appwriteTableId": { "type": "string" },
                            "appwriteBucketId": { "type": "string" },
                            "appwriteDatabaseId": { "type": "string" },
                            "appwriteProjectId": { "type": "string" },
                            "appwriteEndpoint": { "type": "string" },
                            "appwriteDevKey": { "type": "string" },
                            "scope": {
                                "oneOf": [
                                    { "type": "string", "enum": ["user", "team"] },
                                    { "type": "array", "items": { "type": "string", "enum": ["user", "team"] } }
                                ]
                            },
                            "scopeColumn": {
                                "description": "Column name(s) `scope` filters/writes on. Default teamId/userId. A string sets both; an object sets them independently.",
                                "oneOf": [
                                    { "type": "string" },
                                    {
                                        "type": "object",
                                        "properties": {
                                            "team": { "type": "string" },
                                            "user": { "type": "string" }
                                        },
                                        "additionalProperties": false
                                    }
                                ]
                            },
                            "autoInjectUserId": { "type": "boolean" },
                            "autoInjectTeamId": { "type": "boolean" },
                            "queries": {
                                "type": "array",
                                "items": { "type": "string" }
                            },
                            "storage": {
                                "type": "object",
                                "description": "Map of storage-bucket source names to the column on this row that holds their file IDs.",
                                "additionalProperties": { "type": "string" }
                            },
                            "throttle": { "type": "number" },
                            "cleanupInterval": { "type": "number" },
                            "minChangeThreshold": { "type": "number" },
                            "idleThreshold": { "type": "number" },
                            "enableVisualRendering": { "type": "boolean" },
                            "includeVelocity": { "type": "boolean" },
                            "persist": {
                                "description": "Persist this source's rows in IndexedDB and hydrate them before the network on the next visit (stale until the first fresh landing). `true` = boot tier with defaults.",
                                "oneOf": [
                                    { "type": "boolean" },
                                    {
                                        "type": "object",
                                        "properties": {
                                            "tier": {
                                                "type": "string",
                                                "enum": ["boot", "lazy"],
                                                "default": "boot",
                                                "description": "boot: hydrate during data-plugin init, before the first paint; lazy: hydrate on the first $x.<source> read."
                                            },
                                            "maxRows": {
                                                "type": "integer",
                                                "minimum": 1,
                                                "default": 1000,
                                                "description": "Rows kept at write time (the most recent by `recent`)."
                                            },
                                            "recent": {
                                                "type": "string",
                                                "description": "Field that orders recency for `maxRows` (default $updatedAt if present, else insertion order)."
                                            },
                                            "ttl": {
                                                "oneOf": [{ "type": "string" }, { "type": "number" }],
                                                "default": "7d",
                                                "description": "Snapshot lifetime: a duration string (`7d`, `12h`, `30m`) or milliseconds. Expired entries are ignored and deleted."
                                            },
                                            "strip": {
                                                "type": "array",
                                                "items": { "type": "string" },
                                                "description": "Field names or glob patterns removed from every row before it is written. `*secret*`, `*token*`, `*password*` and `credentials*` are always stripped."
                                            }
                                        },
                                        "additionalProperties": false
                                    }
                                ]
                            }
                        },
                        "additionalProperties": true
                    }
                ]
            }
        },
        "persistence": {
            "type": "object",
            "description": "Persisted $x settings (sources opt in with `persist`).",
            "properties": {
                "scope": {
                    "type": "string",
                    "description": "Expression evaluated with the magics available (e.g. \"$auth.currentTeam?.$id\"); re-evaluated on every manifest:auth:* event. Snapshots are keyed by its value; a change wipes the previous scope and rehydrates. Omit for a single-tenant scope."
                }
            },
            "additionalProperties": false
        },
        "appwrite": {
            "type": "object",
            "description": "Appwrite global configuration. Per-source overrides live on the data entry.",
            "properties": {
                "projectId": { "type": "string" },
                "endpoint": { "type": "string", "format": "uri" },
                "databaseId": { "type": "string" },
                "devKey": {
                    "type": "string",
                    "description": "May reference an env var via ${VAR_NAME}."
                },
                "auth": {
                    "type": "object",
                    "properties": {
                        "methods": {
                            "type": "array",
                            "items": {
                                "type": "string",
                                "enum": ["guest", "guest-auto", "guest-manual", "magic", "otp", "oauth", "password"]
                            }
                        },
                        "teams": {
                            "type": "object",
                            "properties": {
                                "permanent": { "type": "array", "items": { "type": "string" } },
                                "template": { "type": "array", "items": { "type": "string" } },
                                "guests": { "type": "boolean", "description": "Seed permanent/template teams for guest (anonymous) sessions too. Off by default." },
                                "authenticated": { "type": "boolean", "description": "Seed permanent/template teams for authenticated (non-anonymous) sessions. On by default; set false (with guests:true) to make seeding guest-only." }
                            }
                        },
                        "guestUpgrade": { "type": "boolean", "description": "Preserve a guest's account (and its teams) when they sign in, instead of replacing it. Works with magic links and OAuth; email OTP cannot convert anonymous accounts (Appwrite limitation). Defaults to the value of teams.guests." },
                        "guestMigration": {
                            "type": "object",
                            "description": "Carry a guest's teams over to the account they sign into via email OTP (which cannot convert in place). Requires the deployed guest-migration Appwrite Function — see templates/guest-migration-function.",
                            "properties": {
                                "functionId": { "type": "string", "description": "Id of the deployed guest-migration function." }
                            }
                        },
                        "roles": {
                            "type": "object",
                            "additionalProperties": {
                                "type": "object",
                                "additionalProperties": {
                                    "type": "array",
                                    "items": { "type": "string" }
                                }
                            }
                        },
                        "creatorRole": { "type": "string" },
                        "creatorRoles": {
                            "description": "Role(s) the team creator is assigned atomically at creation. null or [] = owner-only (creator holds just Appwrite's intrinsic owner; no template role). Takes precedence over creatorRole.",
                            "oneOf": [
                                { "type": "string" },
                                { "type": "array", "items": { "type": "string" } },
                                { "type": "null" }
                            ]
                        }
                    }
                }
            }
        },
        "status": {
            "type": "object",
            "description": "Named, top-level-addressable status signals exposed via $status.<name>. Each entry rolls up one or more underlying signals into a single health state. Signal layer only — the author renders their own UI.",
            "additionalProperties": {
                "oneOf": [
                    {
                        "type": "string",
                        "description": "Shorthand: an HTTP URL probed for reachability/latency."
                    },
                    {
                        "type": "array",
                        "description": "Multiple signals rolled up (worst-of by default).",
                        "items": { "$ref": "#/definitions/statusSignal" }
                    },
                    {
                        "type": "object",
                        "description": "A single signal, or an aggregate via `signals` with rollup options.",
                        "properties": {
                            "signals": {
                                "type": "array",
                                "items": { "$ref": "#/definitions/statusSignal" }
                            },
                            "rollup": {
                                "type": "string",
                                "enum": ["worst", "best"],
                                "default": "worst"
                            },
                            "refresh": {
                                "type": "number",
                                "description": "Poll interval in milliseconds (default 30000)."
                            },
                            "confirmations": {
                                "type": "number",
                                "description": "Consecutive observations required before a state change (hysteresis)."
                            },
                            "staleAfter": {
                                "type": "number",
                                "description": "Milliseconds without a known signal before the entry reads as stale (default refresh × 3)."
                            }
                        },
                        "additionalProperties": true
                    }
                ]
            }
        },
        "payments": {
            "type": "object",
            "description": "Payments plugin config. Presence of this block auto-loads the payments plugin ($pay / x-pay).",
            "properties": {
                "provider": {
                    "type": "string",
                    "description": "Payment adapter name (built-in or registered)."
                },
                "endpoint": {
                    "type": "string",
                    "description": "Backend endpoint for checkout/portal/state calls. Supports env() references."
                }
            },
            "additionalProperties": true
        },
        "ai": {
            "description": "Chat plugin config. Presence (object or bare true) auto-loads the chat plugin ($chat). Custom-adapter projects can use true with no config.",
            "oneOf": [
                { "type": "boolean" },
                {
                    "type": "object",
                    "additionalProperties": true,
                    "properties": {
                        "persist": {
                            "description": "Persisted conversation windows on the shared IndexedDB store: the last `messages` of the `conversations` most recently opened conversations, hydrated (stale) before the adapter load. Off by default; true = defaults.",
                            "oneOf": [
                                { "type": "boolean" },
                                {
                                    "type": "object",
                                    "additionalProperties": false,
                                    "properties": {
                                        "messages": { "type": "integer", "minimum": 1, "default": 50 },
                                        "conversations": { "type": "integer", "minimum": 1, "default": 30 },
                                        "ttl": { "type": ["string", "number"], "default": "7d", "description": "Duration string (ms|s|m|h|d|w) or milliseconds." },
                                        "strip": { "type": "array", "items": { "type": "string" }, "description": "Message field names, globs or dotted paths removed before writing, on top of the always-on secret patterns." }
                                    }
                                }
                            ]
                        }
                    }
                }
            ]
        },
        "chat": {
            "description": "Alias of ai — presence auto-loads the chat plugin ($chat).",
            "oneOf": [
                { "type": "boolean" },
                {
                    "type": "object",
                    "additionalProperties": true,
                    "properties": {
                        "persist": {
                            "description": "Persisted conversation windows on the shared IndexedDB store: the last `messages` of the `conversations` most recently opened conversations, hydrated (stale) before the adapter load. Off by default; true = defaults.",
                            "oneOf": [
                                { "type": "boolean" },
                                {
                                    "type": "object",
                                    "additionalProperties": false,
                                    "properties": {
                                        "messages": { "type": "integer", "minimum": 1, "default": 50 },
                                        "conversations": { "type": "integer", "minimum": 1, "default": 30 },
                                        "ttl": { "type": ["string", "number"], "default": "7d", "description": "Duration string (ms|s|m|h|d|w) or milliseconds." },
                                        "strip": { "type": "array", "items": { "type": "string" }, "description": "Message field names, globs or dotted paths removed before writing, on top of the always-on secret patterns." }
                                    }
                                }
                            ]
                        }
                    }
                }
            ]
        },
        "render": {
            "type": "object",
            "description": "Static rendering configuration consumed by mnfst-render.",
            "properties": {
                "localUrl": { "type": "string", "format": "uri" },
                "liveUrl": { "type": "string", "format": "uri" },
                "output": { "type": "string", "default": "dist" },
                "routerBase": { "type": "string", "default": "" },
                "locales": {
                    "type": "array",
                    "items": { "type": "string" }
                }
            },
            "additionalProperties": true
        },
        "device": {
            "type": "object",
            "description": "Opts the app into the device plugin: $share, $secure, $links, $push, $app, $haptics, $biometric and $camera, each with a web fallback and Capacitor as the native adapter. Auto-loaded inside a Capacitor container and whenever a page uses one of those magics; an empty object loads it on every build.",
            "additionalProperties": true
        },
        "native": {
            "type": "object",
            "description": "Deprecated alias of `device` (renamed in 0.5.199); still accepted.",
            "additionalProperties": true
        },
        "utilities": {
            "type": "object",
            "description": "Utilities-plugin overrides for classes a plain HTML/component scan can't find on its own — e.g. built from a runtime value (`:class=\"ok ? 'bg-green-500' : 'bg-amber-500'\"`). `safelist` classes are baked into the static utilities sheet (scanClasses/compileUtilities) and treated as covered by the runtime; `patterns` classes are trusted as covered by the runtime only (a regex can't be baked).",
            "properties": {
                "safelist": {
                    "type": "array",
                    "items": { "type": "string" },
                    "description": "Exact utility class names, e.g. [\"bg-amber-500\", \"bg-green-500\"]."
                },
                "patterns": {
                    "type": "array",
                    "items": { "type": "string" },
                    "description": "Regex source strings matched against each class token, e.g. \"^bg-(red|green|blue)-[0-9]+$\"."
                }
            },
            "additionalProperties": false
        }
    },
    "definitions": {
        "statusSignal": {
            "description": "One health input. Provider is inferred by which field is present.",
            "oneOf": [
                {
                    "type": "string",
                    "description": "An HTTP URL probed for reachability/latency."
                },
                {
                    "type": "object",
                    "additionalProperties": true,
                    "properties": {
                        "label": { "type": "string" },
                        "url": { "type": "string", "description": "HTTP probe target." },
                        "expect": { "type": "number", "description": "Exact status code to treat as operational (default: any 2xx)." },
                        "degradedAbove": { "type": "number", "description": "Latency in ms above which the probe reads as degraded." },
                        "method": { "type": "string" },
                        "headers": { "type": "object", "additionalProperties": { "type": "string" } },
                        "timeout": { "type": "number" },
                        "static": { "type": "string", "description": "Literal state (operational | degraded | partial_outage | major_outage | maintenance)." },
                        "feed": { "type": "string", "description": "URL returning JSON whose state field is read." },
                        "path": { "type": "string", "description": "Dot-path into a feed response to the state value." },
                        "mirror": { "type": "string", "description": "Named upstream whose own status feed is reflected (e.g. 'appwrite')." },
                        "appwriteService": { "type": "string", "description": "Resolves against the global appwrite endpoint's /health." },
                        "mcp": { "type": "string", "description": "Hosted (Manifest MCP) signal; a feed URL when self-resolving." },
                        "heartbeat": { "type": "string", "description": "Dead-man's-switch key; call $status.beat(key) to pulse." },
                        "expectEvery": { "type": "number", "description": "Heartbeat window in ms before it reads as down." }
                    }
                }
            ]
        }
    }
}
