# Build an application

> Publish code that Apigami hosts and runs — actions, a database, triggers — and reuse what other people have published. One object to learn - a version.

An **application** is code Apigami hosts for you. It answers HTTP, it can hold a database, it can run
on a schedule, and it can use the session of a user who approved it — or call another application
that already knows how. You write it, push it, run it and publish it without a server, a domain or a
development machine of your own.

One object carries all of it: a **version**.

> An application is an HTTP server with several routes. A version holds all of its files and all of
> its actions. Naming an action only ever means running that one.

There is no separate source, no build to track, no channel to point at. You write a version, you run
one of its actions, you deploy it, you put it in production. Rolling back is putting an earlier
version in production — the same call.

If your AI assistant is connected to Apigami over MCP, every step in these pages is one tool call
(`app_settings`, `app_version_write`, `app_run`, `app_deploy`, `app_publish`). Everything those tools
do, the `/v1` routes in these pages do, so nothing is available only through an assistant.

## The credentials in the examples

`$TOKEN` is the credential of a member of your team, sent as `Authorization: Bearer $TOKEN` to the
`/v1/apps` routes that create, write, deploy and publish: an agent token from an [agent
sign-in](/docs/mcp/#agent-sign-in), which is how an AI assistant in a terminal, on a server or in CI
gets one (in a hosted chat or a sandbox, the person adds the
[connector](/docs/ai-agents/#ask-the-person-to-add-the-connector) instead), a personal access token, or the access token of a signed-in session. Running an action over HTTP takes
`$API_KEY` instead, an [API key](/docs/authentication/) of the application, which a person creates in
the dashboard; an assistant holding an agent token runs actions with the MCP tool `app_run`. `$APP_ID`
is the `id` that creating the application returns.

## Starter template: hello world without a browser

The smallest complete application: `GET /healthz`, one action that answers JSON, and a home page. It
reaches no site through a browser, so a person connects to it with their email alone, from any
device. Three files; replace the `slug` with a name of your own first, since a slug is unique on all
of Apigami.

```json title="manifest.json"
{
  "openapi": "3.1.0",
  "info": {
    "title": "Hello",
    "description": "Greets the person using it, by name.",
    "version": "1.0.0"
  },
  "x-apigami": {
    "manifest_version": 2,
    "slug": "acme-hello",
    "entrypoint": "main.ts",
    "pages": [{ "kind": "home", "id": "home", "title": "Hello", "page_url": "/" }]
  },
  "paths": {
    "/hello": {
      "post": {
        "operationId": "sayHello",
        "summary": "Say hello",
        "description": "Answers a greeting for the name given, and the user_ref of the person it serves.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": { "type": "string", "maxLength": 80, "description": "Who to greet, world when left out" }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The greeting",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["message", "user_ref"],
                  "properties": {
                    "message": { "type": "string" },
                    "user_ref": { "type": ["string", "null"], "description": "The person served, null when nobody is connected" }
                  }
                },
                "example": { "message": "Hello, Ada!", "user_ref": "usr_3f9c1a0e7b2d4c58a61e9f0b2d7c4e13" }
              }
            }
          }
        }
      }
    }
  }
}
```

`main.ts` exports `default { fetch(request, env) }`, reads `APIGAMI_API_URL` from `env` and imports
`home.html` as text: nothing reaches the network while the module loads.

```ts title="main.ts"
import home from './home.html' with { type: 'text' }

const PAGE_HEADERS = {
  'content-type': 'text/html; charset=utf-8',
  'x-content-type-options': 'nosniff',
  'referrer-policy': 'no-referrer',
  'cross-origin-opener-policy': 'same-origin-allow-popups',
  'x-robots-tag': 'noindex',
}

const sayHello = async (request: Request, env) => {
  // Who is asking comes from the run token Apigami hands the action, never from its input.
  const token = request.headers.get('X-Apigami-Run')
  const me = await fetch(`${env.APIGAMI_API_URL}/v1/me`, { headers: { Authorization: `Bearer ${token}` } })
  if (!me.ok) return Response.json({ error: 'unauthorised' }, { status: 401 })
  const { user_ref } = await me.json()
  const input = await request.json().catch(() => ({}))
  const name = typeof input?.name === 'string' && input.name.trim() ? input.name.trim().slice(0, 80) : 'world'
  return Response.json({ message: `Hello, ${name}!`, user_ref })
}

export default {
  async fetch(request: Request, env) {
    const { pathname } = new URL(request.url)
    if (request.method === 'GET' && pathname === '/healthz') return Response.json({ ok: true })
    if (request.method === 'GET' && pathname === '/') return new Response(home, { headers: PAGE_HEADERS })
    if (request.method === 'POST' && pathname === '/hello') return sayHello(request, env)
    return Response.json({ error: 'not found' }, { status: 404 })
  },
}
```

```html title="home.html"
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Hello</title>
<h1>Hello</h1>
<form id="hello">
  <input id="name" placeholder="Your name" maxlength="80">
  <button>Say hello</button>
</form>
<p id="answer"></p>
<script src="https://api.eu.apigami.com/apigami.js"></script>
<script type="module">
  // Nothing personal is rendered here: the greeting comes from the action, under the person's token.
  // ready() waits for the session, sending a visitor through Apigami's connect page and back first.
  document.getElementById('hello').addEventListener('submit', async (event) => {
    event.preventDefault()
    try {
      await apigami.ready()
      const { message } = await apigami.call('sayHello', { name: document.getElementById('name').value })
      document.getElementById('answer').textContent = message
    } catch (error) {
      document.getElementById('answer').textContent = String(error)
    }
  })
</script>
```

With the MCP tools: `app_settings` with a name, then `app_version_write` with the three files, then
`app_run` with the action `sayHello`, `app_deploy`, `app_run` again with `version` on the deployed
version, and `app_publish`, which refuses a version never run once deployed. Over HTTP, create the
application, then write the version from the three files in one call:

```sh
APP_ID=$(curl -s -X POST https://api.eu.apigami.com/v1/apps \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"name":"Hello"}' | jq -r .id)

jq -n --rawfile manifest manifest.json --rawfile main main.ts --rawfile home home.html '{
  base_version_id: null,
  changes: [
    { op: "write", path: "manifest.json", content: $manifest },
    { op: "write", path: "main.ts", content: $main },
    { op: "write", path: "home.html", content: $home }
  ]
}' | curl -s -X POST "https://api.eu.apigami.com/v1/apps/$APP_ID/versions" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' --data-binary @-
```

The answer carries the `version_id`; then run, deploy and publish it as
[Run it, deploy it, publish it](/docs/app-publish/#run-it-deploy-it-publish-it) shows. The address to give people is
`https://app.apigami.com/connect?app_id=APP_ID`: it connects them, then opens the home page. The home page's own
address works too: when the page needs a person, it sends a visitor through the same connection, and back.

## The guide, page by page

Read them in this order the first time; each one stands on its own afterwards.

1. [The manifest](/docs/app-manifest/): `manifest.json`, the sites an application reaches and the name
   it goes by.
2. [Write the server](/docs/app-server/): the HTTP server whose routes are the actions, what an action
   receives and how it reaches a site.
3. [Push and publish a version](/docs/app-publish/): send the files, fix what the validation found,
   try an action, put a version in production, find out what failed.
4. [Side panel, home and settings pages](/docs/app-pages/): the screens an application serves.
5. [The person behind a page](/docs/app-page-sessions/): which person a page serves, on the server and
   in the page, and how long a page keeps its access.
6. [Database and schedules](/docs/app-database/): a SQLite database per application, PostgreSQL when it
   needs one, and actions that run on their own.
7. [Sign in to other services](/docs/app-sign-in/): a person's account elsewhere, and asking them to
   set up or reconnect.
8. [Call other applications](/docs/app-dependencies/): use what another application already does, and
   show its settings.
9. [Your own address](/docs/app-domain/): serve production at an address of yours.
10. [Example: watch a page and show what is new](/docs/app-example-watch/): a complete application, a
    trigger reading a page through the person's browser, its database and a home page.