Skip to content

The manifest

Markdown

Every application starts from one file, manifest.json. It names the actions, the sites the application reaches and the name it goes by.

A project is a directory. One file is required, manifest.json at its root: an OpenAPI 3.1 document that describes your actions, plus an x-apigami block that tells Apigami how to run them.

manifest.json
{
"openapi": "3.1.0",
"info": {
"title": "Grocery",
"description": "Search a grocery site and fill a basket, using the shopper's own session.",
"version": "1.0.0"
},
"x-apigami": {
"manifest_version": 2,
"slug": "acme-basket",
"entrypoint": "main.ts",
"domains": [{ "domain": "example.com", "via": "extension" }],
"env": [
{ "name": "CURRENCY", "description": "ISO 4217 code shown in results", "secret": false }
]
},
"paths": {
"/search": {
"post": {
"operationId": "searchProducts",
"summary": "Search products",
"description": "Finds products by name and returns their price and reference.",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": ["query"],
"properties": {
"query": { "type": "string", "description": "What to look for" },
"limit": { "type": "integer", "minimum": 1, "maximum": 50, "default": 10 }
}
}
}
}
},
"responses": {
"200": {
"description": "The products found",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": ["products"],
"properties": {
"products": {
"type": "array",
"items": {
"type": "object",
"required": ["reference", "name", "price"],
"properties": {
"reference": { "type": "string" },
"name": { "type": "string" },
"price": { "type": "number", "description": "Unit price, in the currency of CURRENCY" }
}
}
}
}
},
"example": { "products": [{ "reference": "7412", "name": "Comté 18 mois", "price": 4.35 }] }
}
}
}
}
}
},
"/basket": {
"post": {
"operationId": "readBasket",
"summary": "Read the basket",
"description": "Returns what is in the shopper's basket right now.",
"responses": {
"200": {
"description": "The basket",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": ["lines", "total"],
"properties": {
"lines": {
"type": "array",
"items": {
"type": "object",
"properties": {
"reference": { "type": "string" },
"quantity": { "type": "integer" }
}
}
},
"total": { "type": "number" }
}
}
}
}
}
}
}
}
}
}

Two version numbers live here and they are not the same thing. x-apigami.manifest_version is the version of this format, and it is 2; info.version is the version of your application, a label you choose and that nothing depends on. What identifies a version to Apigami is its version_id.

summary, description and responses are optional in OpenAPI and required here. They are what another developer — and another developer’s assistant — reads to decide whether to reuse your work, and a brick nobody can read is a brick nobody reuses. Say in description what the action is worth to whoever calls it, not how it is implemented.

What an action takes and answers has to have a shape. A success response is any 2xx code, 201 included, or 2XX; the lowest numbered 2xx code it declares, else its 2XX, needs a content naming its media type, and where that media type is application/json, a schema of what comes back; the same holds for the requestBody of an action that takes one. A description alone is refused: catalog_read hands your manifest to another group’s assistant as the whole contract, and a sentence where a shape was promised means one call made blind to find out what the fields are called. Not everything is JSON — an action that answers a PDF, a CSV or raw bytes declares application/pdf, text/csv or application/octet-stream with no schema at all, and that is enough. An example is never required and is worth writing wherever the shape alone does not say what a good answer looks like.

x-apigami.public_paths is no longer needed: the --staging address serves every path to anyone, and a manifest that still lists it is accepted and ignored.

Dependencies are not in the manifest. Being allowed to call another application is an authorisation stored by Apigami, granted and revoked without deploying anything — see Dependencies: reuse a brick.

x-apigami.domains lists the sites your application talks to, each with how it reaches them:

  • "via": "extension": through the person’s browser, with their session, by request.fetch or a tabs.* operation. The person approves it when they connect, or in a consent window at the first call. An operation your code sends on a domain not declared this way is refused before any window opens: 403 … is not a domain this application declares as reached through the browser. A side panel shows only on such a domain.
  • "via": "direct": your server calls the site itself, with fetch and its own credentials, an API key in x-apigami.env for instance, never the person’s session. No browser, no consent window.

Only extension is enforced when your code runs: Apigami does not filter what your server fetches, so a direct declaration is not required for the code to work. Declare every site your server calls anyway: the connect page lists it, « Without your browser », so the person sees where their data goes before connecting, catalog_read shows it to whoever considers reusing your application, and an application that uses yours shows it in turn. When every domain, yours and those of the applications you use, is direct, a person connects without attaching a browser.

Declare the site’s own domain, example.com, not www.example.com. A domain is a bare name, lowercased: a scheme, a path, a port, a wildcard such as *.example.com or a trailing dot is refused with Use a bare domain, such as example.com. The person’s approval of example.com covers it and every subdomain, www.example.com and api.example.com included; an approval of www.example.com covers www.example.com and its own subdomains, but neither example.com nor api.example.com. Pass the declared string as params.domain in every call, and the host you want in url:

{ "op": "request.fetch", "params": { "domain": "example.com", "url": "https://www.example.com/news" } }

params.domain must equal a declared domain exactly: with example.com declared, "domain": "www.example.com" answers 403 www.example.com is not a domain this application declares as reached through the browser. The url must be https:// on that domain or one of its subdomains: outside it the call answers 502 url is outside the granted domain example.com: …, and a redirect that leaves it 502 redirected outside the granted domain example.com: ….

x-apigami.permissions lists what your application may do beyond its sites: "emails" and "ai". With "emails", your code may email the people connected to it with the emails.send operation, by their user_ref, without ever learning their address.

"x-apigami": { "permissions": ["emails"] }

It belongs to the version that declares it: production’s for everyone, the latest version’s when a member of your group previews an email or sends one to themself. Adding or removing it takes a deployment, and an application without hosted code has no manifest, so it sends no email. An application that uses yours as a brick inherits it, and the email always leaves in the name of the application the person connected to. Once it is declared, the connect page asks each person who connects to tick the box of your emails, and offers a second, optional box for your news. How to write them: Emails to your users.

"ai": your code may ask a model to read text, PDFs and images with the ai.generate operation, which Apigami runs in the EU, without storing what it reads or training on it.

"x-apigami": { "permissions": ["ai"] }

The version whose code calls must declare it: the one a run executes, production’s for your apg_ key and APIGAMI_KEY. The connect page lists it among what your application may do, without a box to tick, and so does it for an application that uses yours as a brick. How to call it: AI in your application.

"database": "sqlite", or true, gives your application a SQLite database, which your code reads as env.DB; "database": "postgres" gives it a PostgreSQL database, reached through env.DATABASE_URL, for a real need only: a schema or a library already written for PostgreSQL, long queries, many concurrent writes. Either is created at the first try or deployment of a version that declares it, and the application keeps that kind. Without one, the application has none: a site that only serves pages needs nothing more. Which to take, and how to use it: Database and schedules. Deleting the application deletes a PostgreSQL database at once; a SQLite database is kept 30 days, during which contact@apigami.com can give it back, then deleted.

"manifest_version": 1 is refused (manifest_v1_retired): Apigami no longer runs it, and an older version written for it can be neither tried nor deployed. x-apigami.predeploy is refused too (predeploy_removed): Migrations replace it.

The slug: its rules, and what a rename does (no redirect)

Section titled “The slug: its rules, and what a rename does (no redirect)”

x-apigami.slug is the public name of your code and its address, https://{slug}.apigami.tech. It is unique on all of Apigami, first come, first served: choose a distinctive one, your product’s name with what the application does, such as acme-basket, never a common word another team will want too. It is 3 to 32 lowercase letters, digits and hyphens, with no -- and no hyphen at either end. A few whole names are refused, platform words and large brands, and so is a slug with apigami, staging, login, signin, password, official, com, net or org between its hyphens. A version whose slug is reserved or taken is invalid as soon as you write it, with reserved_slug or slug_taken.

The slug is reserved at your first deploy, never by creating the application, and it is never freed afterwards: a slug you renamed away from stays your group’s, its host answering 404 until a version declares it again, and a deleted application’s slug stays with your group, which alone can take it back. Code and dependencies name an application by its app_id, never by its slug.

A new slug becomes the address when the version carrying it is published; until then, a deployment of it answers at --staging under both names. The publication’s answer then carries rename, with what does not follow the application to its new host: browser storage, cookies and sessions, an installed app, the redirect URIs registered with other services, and the {slug}.* prefix of your event types. The old host and its --staging answer 404 from then on, without a redirect: the name stays your group’s, and only a version declaring it again brings it back. When the hosting provider fails mid-move, rename.note says the move is not finished: the old host and its --staging may still serve the previous version or already answer 404, and publishing the same version again finishes it.