> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tappify.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Deploy your server

> Run the starter's handler on Cloudflare Workers or Vercel, and point your manifest at it.

Tappify never runs your code on its servers. An extension that answers procedures, syncs
metrics or performs actions runs its own server, and the starter ships one handler behind two
entries.

```bash theme={null}
pnpm run deploy:worker
tappify extension set server.baseUrl https://my-extension-server.<your-subdomain>.workers.dev
```

## One handler, two entries

`server/handler.ts` exports the options and the handler:

```ts theme={null}
import {
  createTappifyHandler,
  type TappifyFetchHandler,
  type TappifyHandlerOptions,
} from "@tappify/extension-sdk/server";
import { procedures } from "./procedures";

export const handlerOptions: TappifyHandlerOptions = {
  extensionId: "starter",
  health: () => ({ ok: true, version: "0.1.0" }),
  procedures,
};

export const handler: TappifyFetchHandler = createTappifyHandler(handlerOptions);
```

It is a `(Request) => Promise<Response>`, so it runs anywhere the Fetch API does. The options
are exported separately because a platform that mounts you under a prefix needs `basePath`.

<Tabs>
  <Tab title="Cloudflare Workers">
    `server/worker.ts`:

    ```ts theme={null}
    import { handler } from "./handler";

    export default { fetch: handler };
    ```

    `wrangler.toml`:

    ```toml theme={null}
    name = "my-extension-server"
    main = "server/worker.ts"
    compatibility_date = "2026-09-01"
    compatibility_flags = ["nodejs_compat"]

    [observability]
    enabled = true
    ```

    ```bash theme={null}
    pnpm run deploy:worker
    ```
  </Tab>

  <Tab title="Vercel">
    `api/tappify/[...path].ts`:

    ```ts theme={null}
    import { createTappifyHandler } from "@tappify/extension-sdk/server";
    import { handlerOptions } from "../../server/handler";

    export const config = { runtime: "edge" };

    export default createTappifyHandler({ ...handlerOptions, basePath: "/api" });
    ```

    Vercel serves the file at `/api/tappify/…`, so `basePath` is what makes the handler see
    the `/tappify/…` paths it routes on. Deploy the repository and set `server.baseUrl` to
    `https://your-app.vercel.app/api`.
  </Tab>

  <Tab title="Express or Node">
    ```ts theme={null}
    import express from "express";
    import { toExpress } from "@tappify/extension-sdk/server";
    import { handler } from "./handler";

    const app = express();
    app.use(toExpress(handler));
    app.listen(3000);
    ```

    `toNode(handler)` returns a plain `http` request listener for a server without Express.
  </Tab>
</Tabs>

## Point the manifest at it

```bash theme={null}
tappify extension set server.baseUrl https://my-extension-server.<your-subdomain>.workers.dev
tappify extension set server.sandboxBaseUrl https://my-extension-staging.<your-subdomain>.workers.dev
```

`sandboxBaseUrl` is used for dev installs and sandbox projects. It defaults to `baseUrl`.
Both must be `https`.

## The routes Tappify calls

| Route                              | Called when                                     |
| ---------------------------------- | ----------------------------------------------- |
| `GET /tappify/health`              | At every publish, once per base url you declare |
| `POST /tappify/procedures/:name`   | Your UI called `tap.server.<name>`              |
| `POST /tappify/metrics`            | Connector sync, on your declared cadence        |
| `POST /tappify/tools/:toolId`      | The assistant called one of your tools          |
| `GET /tappify/mentions/:mentionId` | The owner opened the mention picker             |
| `POST /tappify/actions/:actionId`  | The owner approved an action                    |
| `GET /tappify/context/:contextId`  | A chat turn started, or a mention resolved      |
| `POST /tappify/events`             | A Tappify event your manifest subscribes to     |
| `POST /tappify/work/:operation`    | A work-destination operation                    |

`createTappifyHandler` routes all of them; you fill in the ones you declared. A path it routes
with no handler registered comes back as a 404 naming the missing one, so a contribution you
declared and never wired is loud rather than silent.

## Verifying the caller

Every route except health carries `Authorization: Bearer <install token>` — RS256, five
minutes, audience `ext:<your extension id>` — and `X-Tappify-Event-Id`, the idempotency key.
The handler verifies the token for you against Tappify's JWKS. Verify it yourself only when
you are not using the handler:

```ts theme={null}
import { verifyTappifyToken } from "@tappify/extension-sdk/server";

const claims = await verifyTappifyToken(token, { extensionId: "starter" });
```

The keys are at
`https://api.tappify.ai/api/v1/extensions/.well-known/jwks.json` and rotate with a 24-hour
overlap, so both keys are served through the change.

## Before you publish

Health has to answer 200 within three seconds, on `baseUrl` and on `sandboxBaseUrl` if you
declared one, or the publish stops. A redirect counts as a failure: serve the route from the
url you declared.

`tappify extension dev --live` serves your bundle from your machine but still relays
procedures to the url in your manifest. Run the handler locally with `pnpm run dev:worker`,
give it a public address, and pass that to `--server <url>` for the length of the session —
Tappify calls you from its own servers, so a `localhost` url is not one it can reach. See
[Local server during live preview](/extensions/test/local-server).
