# App manifest The permission contract of an app version. Every field, generated from the schema the API and the gate validate with, and an example with the sentences a person reads before approving. URL: https://docs.qaldy.com/reference/manifest/ Every app version carries a **manifest**. The builder proposes it, a person approves it, and the gate enforces it: **anything not declared here is denied.** The manifest of a published version never changes; wider access is a new version. You rarely write one by hand. A plan from the Build screen, `plan_app_from_prompt` or `qaldy build --plan` contains one, and shows it as sentences. This page is for reading a plan closely, for reviewing what an agent proposes, and for building tools around the API. ## An example ```json { "name": "Low-stock alert", "description": "Tells the roastery chat in Telegram when a coffee bean stock drops below 5 kg.", "archetype": "radiotower", "category": "messaging", "permissions": { "data": [ { "table": "stock", "access": "read" }, { "table": "alerts_sent", "access": "write" } ], "connectors": [ { "id": "telegram", "actions": [ "sendMessage" ], "scope": "#roastery" } ], "egress": [], "ai": null }, "triggers": [ { "type": "schedule", "cron": "0 8 * * 1-5" } ], "sharing": { "default": "private" }, "roi": { "manualMinutesPerRun": 10 } } ``` The plan shows this manifest to the person who approves it as these sentences: - Can read **stock**. - Can change **alerts_sent**. - Can send Telegram messages to **#roastery** through **Telegram**. - Does not use AI models. - Can't reach anything else on the internet. Nothing here reaches the internet: `egress` is empty and `ai` is `null`. Each connector action is named, so the app can send a message to that one chat and cannot do anything else through Telegram. The app can read the `stock` table and write `alerts_sent`, and no other table. ## Every field The table is generated from the schema the API, the builder and the gate all parse with, so it cannot drift from what is enforced. | Field | Type | Required | Notes | |---|---|---|---| | `name` | string | yes | 2 to 60 characters | | `description` | string | yes | 4 to 280 characters | | `archetype` | "kiosk" · "tower" · "warehouse" · "workshop" · "bank" · "townhall" · "clocktower" · "printhouse" · … | yes | | | `category` | "interface" · "data" · "messaging" · "money" · "ai" · "time" · "people" · "growth" | yes | | | `permissions` | object | yes | | | `permissions.data` | object[] | yes | | | `permissions.data[].table` | string | yes | 1 to 48 characters | | `permissions.data[].access` | "read" · "write" | yes | | | `permissions.connectors` | object[] | yes | | | `permissions.connectors[].id` | "shop" · "stripe" · "gmail" · "telegram" · "sheets" · "slack" · "kaspi" · "1c" · … | yes | | | `permissions.connectors[].actions` | string[] | yes | | | `permissions.connectors[].scope` | string | | up to 80 characters | | `permissions.egress` | object[] | yes | | | `permissions.egress[].domain` | string | yes | 3 to 253 characters | | `permissions.egress[].reason` | string | yes | 8 to 200 characters | | `permissions.ai` | object or null | yes | | | `triggers` | object[] | yes | | | `sharing` | object | yes | | | `sharing.default` | "private" · "team" · "workspace" | yes | | | `roi` | object | yes | | | `roi.manualMinutesPerRun` | number | yes | 0 to 1440 | ## How the permissions are enforced | Field | Enforced by | |---|---| | `permissions.data` | The runtime host builds the app's data binding from it. A table not listed does not exist for the app, and `write` is separate from `read`. | | `permissions.connectors` | The gate matches each call to `{connector, action}` in the version's manifest. Anything else is `403 egress_blocked` (`action_not_declared`). | | `permissions.egress` | The gate allows exactly these hosts, plus the connectors' own hosts: HTTPS, port 443, an exact name. Each needs a reason a person can read. Wildcards and IP addresses are refused. | | `permissions.ai` | The gate allows model calls only for the declared models, within the monthly budget. An undeclared model is refused. | | `triggers` | The platform starts a run on a schedule, a webhook, a form, a button press or an event. | | `sharing.default` | Who can open a new app at first; sharing then works as [described here](https://docs.qaldy.com/getting-started/share/). | A manifest is checked twice: the builder's static checks against the code (declared egress must equal used egress, see [Approvals and the Trust Score](https://docs.qaldy.com/concepts/approvals/)), and the gate at run time against the actual calls.