> ## 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.

# Call your own server with procedures

> Declare a typed request your UI makes to your own server, relayed by the host.

Your UI never calls your server directly. It calls a declared procedure and the host
relays it, which is what gets you the owner's credentials, no CORS, no server URL in the
browser, and sandbox and production routing without a build flag.

```bash theme={null}
tappify extension add procedure --name getSummary --kind read
```

## The manifest entry

```json theme={null}
{
  "server": {
    "baseUrl": "https://your-server.example.com",
    "procedures": {
      "getSummary": {
        "input": { "$ref": "./schemas/get-summary.input.json" },
        "output": { "$ref": "./schemas/get-summary.output.json" },
        "cache": "5m",
        "kind": "read"
      }
    }
  }
}
```

The command writes this entry. You can hand-edit `tappify.extension.json` instead — the
`$schema` line gives your editor completion and validation, and
[`tappify extension doctor`](/extensions/test/doctor-checks) checks the result.

`cache` is the one field the command leaves out: add `"1m"`, `"5m"` or `"1h"` yourself when
repeating the same input over the same window is safe.

`kind: "read"` is retried twice when the connection fails. `kind: "write"` is never retried,
and neither is a call that timed out. A procedure whose name reads like a write — `create`,
`update`, `delete`, `send`, `set` — without `kind: "write"` is a `doctor` warning.

## Calling it

```tsx theme={null}
import { useTapServer } from "@tappify/extension-sdk";

export function Summary() {
  const summary = useTapServer("getSummary", { days: 7, platform: "all" });

  return <span>{summary.data?.installs ?? 0}</span>;
}
```

or, outside a component, `await tap.server.getSummary({ days: 7, platform: "all" })`. Both
are typed from the two schemas once you have run
[`tappify extension types`](/extensions/build/generated-types).

## Handling it

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

export const handler = createTappifyHandler({
  extensionId: "starter",
  procedures: {
    getSummary: (request) => {
      const { days } = request.input;

      if (days < 1) {
        throw new TapServerError(
          "RANGE_TOO_SHORT",
          "getSummary needs a range of at least one day. Widen the date picker, then retry.",
          400,
        );
      }

      return { installs: days * 180, delta: 0.08 };
    },
  },
});
```

`request` carries everything the host knows:

| Field         | Is                                                                             |
| ------------- | ------------------------------------------------------------------------------ |
| `install`     | `{ id, extensionId, projectId, organizationId }`                               |
| `context`     | `{ projectId, platform, from, to, filters }` — the page's current pickers      |
| `input`       | Validated against your declared input schema before it reaches you             |
| `credentials` | The owner's credentials, when your manifest declares `connector.auth`          |
| `documents`   | Your install- and organization-scoped storage, read-only, including `settings` |
| `claims`      | The verified install token's claims                                            |
| `eventId`     | The idempotency key for this call                                              |
| `request`     | The raw `Request`, when you need a header the fields above do not carry        |

User-scoped documents are the one thing missing from `documents`: they never leave the host.

## What the host does around it

1. Validates your input against the declared schema.
2. Answers from its own cache when the declaration has a `cache` window and the same install
   asked the same thing over the same filters.
3. Mints a five-minute install token and posts to
   `POST {server.baseUrl}/tappify/procedures/getSummary` with that token, the credentials,
   the documents and the current filters. A sandbox project uses `server.sandboxBaseUrl`
   when you declared one.
4. Validates your output against the declared output schema.
5. Caches the result for the declared window.
6. Records the call and its latency on your Runtime page.

Limits: 30 seconds, 1 MB in, 1 MB out, 120 calls per minute per install. A failure arrives
in the UI as `TapServerError { code, message }` carrying your own code and message — the
message is what your component shows, so write one a person can act on.

A procedure is for reads and for your own bookkeeping. Anything that changes the owner's
data in your system is an action, and an action goes through an approval card.

<CardGroup cols={2}>
  <Card title="Deploy your server" icon="server" href="/extensions/build/deploy-your-server">
    Where the handler above runs, and what to set `server.baseUrl` to.
  </Card>

  <Card title="Testing with the SDK" icon="vial" href="/extensions/test/testing-with-the-sdk">
    Drive the handler above with `createTestClient`, the way Tappify calls it.
  </Card>
</CardGroup>
