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

# Your first extension

> Scaffold an extension, run it on your sandbox project, and publish your first release.

Ten minutes from an empty directory to a published release. You need `tappify` installed
and a vendor account — [start there](/extensions/start/create-a-vendor-account) if
`tappify extension whoami` reports neither.

```bash theme={null}
tappify extension init my-extension
cd my-extension
pnpm run dev:worker
cloudflared tunnel --url http://localhost:8787
tappify extension dev --live --server https://<your-tunnel>.trycloudflare.com
```

<Steps>
  <Step title="Scaffold">
    ```bash theme={null}
    tappify extension init my-extension
    ```

    Four questions — id, name, description, category — then the starter is copied and its
    dependencies installed. Pass `--id`, `--name`, `--description` and `--category` to skip
    the questions, or `--no-install` to skip the install.

    The starter copies more than this — a build config, a worker, a test setup. These are
    the files you edit:

    | Path                      | What it is                                                              |
    | ------------------------- | ----------------------------------------------------------------------- |
    | `tappify.extension.json`  | The manifest. Every contribution, scope and schema reference            |
    | `src/widgets/summary.tsx` | A widget in the `kpi-row` slot on Analytics                             |
    | `src/settings.tsx`        | The settings panel owners see on the per-install page                   |
    | `src/tappify.d.ts`        | Generated types. Commit it                                              |
    | `schemas/`                | The JSON Schemas the manifest points at                                 |
    | `server/handler.ts`       | A health check, one procedure, and empty stubs for the assistant routes |
    | `test/`                   | A test for each UI contribution, plus a server test                     |
  </Step>

  <Step title="Run your server locally">
    ```bash theme={null}
    pnpm run dev:worker
    cloudflared tunnel --url http://localhost:8787
    ```

    The widget's number comes from a procedure, and a procedure comes from your server.
    `dev:worker` runs `server/worker.ts` on port 8787, and the tunnel gives it a public
    url — Tappify relays procedures from its own servers, so it needs an address it can
    reach. The server, the tunnel and the dev session each keep running, so each needs its
    own terminal. Any tunnel works,
    and so does a server you have already deployed. See
    [Local server during live preview](/extensions/test/local-server).
  </Step>

  <Step title="Run it on your sandbox project">
    ```bash theme={null}
    tappify extension dev --live --server https://<your-tunnel>.trycloudflare.com
    ```

    Two lines come back: the local remote entry being served, and the host URL to open. The
    URL is your sandbox project's Analytics page, and the widget is in the KPI row with your
    vendor tile on it, showing the installs figure your own machine just answered. Edit
    `src/widgets/summary.tsx` and it reloads.

    `--server` points the relay at your tunnel for the length of the session. Drop it and the
    card reads "The summary did not load", because the manifest still holds the starter's
    placeholder url.

    Without `--live` you get a portal preview URL instead, which needs no install. See
    [The dev server](/extensions/test/dev-server).
  </Step>

  <Step title="Watch it follow the page">
    Change the date picker at the top of Analytics. The number changes, because the widget
    reads `useTapFilters()` and passes the range to its procedure — see
    [Follow the page's filters](/extensions/build/follow-the-filters).
  </Step>

  <Step title="Run the checks">
    ```bash theme={null}
    tappify extension doctor
    ```

    The same checks the Tap Store runs at publish, with a fix line under each failure.
  </Step>

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

    The tunnel ends with the session, so the manifest needs an address that outlives it. The
    starter ships a placeholder, and publish probes `<server.baseUrl>/tappify/health` for a
    200 within three seconds: leave the placeholder and the publish stops there. The starter
    deploys `server/worker.ts` with wrangler; anywhere that answers the health route works.
    See [Deploy your server](/extensions/build/deploy-your-server).
  </Step>

  <Step title="Publish">
    ```bash theme={null}
    tappify extension publish --notes "First release"
    ```

    Two lines: what was sent and its size, and what happens next. The starter is private, so
    the release is live the moment it uploads, installable inside your own workspace and
    nowhere else. Nobody reviews a private release.
  </Step>

  <Step title="Check the release">
    ```bash theme={null}
    tappify extension status
    ```

    The live release, the release in review, the timeline and the install count.
  </Step>
</Steps>

## What just happened

The starter's manifest declares one widget, one settings panel, one hosted-storage
collection and one server procedure. `dev --live` registered a dev install on your sandbox
project pointing at your local remote entry, which the host allowlists for that install
only, and `--server` pointed the procedure relay at your tunnel for as long as the session
ran. `publish` built, zipped, ran the checks and uploaded the bundle, then took the route
the manifest's visibility chose for it.

| Visibility                     | Where a publish goes                                     |
| ------------------------------ | -------------------------------------------------------- |
| Private, the starter's default | Live at once, your own workspace only, never reviewed    |
| Unlisted                       | Reviewed by the automated checks; owners install by link |
| Public                         | Reviewed by an admin, then listed in the Tap Store       |

Once a release is live, the next one skips review whenever it asks for no new scope, no new
contribution kind and no wider visibility — it overwrites the live release where it stands.
Ask for any of the three and it goes through review while the live release keeps serving.

When it is ready for the Tap Store, complete the listing — the long description, one to six
screenshots, and your website, support and privacy urls — and set visibility to public. A
public manifest is not valid without all of them, and that publish comes back in review,
because an admin reads every extension before owners outside your workspace can install it.
See [Your listing](/extensions/publish/your-listing) and
[Visibility](/extensions/publish/visibility).

<CardGroup cols={2}>
  <Card title="Add a widget" icon="grip" href="/extensions/build/add-a-widget">
    The next contribution to add, and where it can render.
  </Card>

  <Card title="Publishing" icon="upload" href="/extensions/publish/publishing">
    What publish checks, and when a release goes live without review.
  </Card>
</CardGroup>
