# Build your first app (/apps/build-your-first-app)

You will build a hosted app with two surfaces: a page in the merchant's admin and a block merchants can place on
their storefront home page. At the end you publish its first release, which goes to review before any store can
install it.

> **The CLI and SDK packages are not on npm yet**
>
> The commands on this page are the ones the `poltura` CLI runs today, checked against its source.
> Until the packages are published, `npm install` of `@poltura/*` fails outside the Poltura team.

Prompt for an AI coding tool:

```text
Build a Poltura app by following https://developers.poltura.com/apps/build-your-first-app.md step
by step, and keep to the rules in https://developers.poltura.com/ai/rules/AGENTS.md. Scaffold it
with `poltura app create my-app --template hosted` and install its dependencies. Change the
storefront block in src/elements.ts and src/workers/storefront-home.tsx as the tutorial shows. Run
`poltura app build` and `poltura app validate` and fix every error until validate prints "ok". Do
not publish; stop and tell me the app is ready for `poltura app publish`.
```

## Before you start [#before-you-start]

- **A Poltura store** where you are an administrator.
- **Developer mode on.** In the admin, open **Settings → Profile** and turn on **Developer mode**.
  **Developer apps** appears in the sidebar.
- **An organization and an app.** In **Developer apps**, choose **Create organization**, then **Create app**.
  Note the app id, a UUID such as `0199f3a2-5b1c-7d4e-9f20-3a4b5c6d7e8f`.
- **A developer token.** On the same page, under **Developer tokens**, choose **Create token** and confirm it's you
  when asked. A token the organization owns starts with `pltd_` and suits CI; one only you own starts with `pltu_`.
  Tokens last 30 days by default; you can pick 7 days, 90 days, 1 year or no expiry. The token is shown once. Keep
  it in an environment variable:

```bash
export POLTURA_APP_TOKEN="pltd_..."
```

- **Node.js 22 or later.**

### Install the CLI [#install-the-cli]

(Not published to npm yet.)

```bash
npm install -g @poltura/cli
# pnpm add -g @poltura/cli
# yarn global add @poltura/cli
# bun add -g @poltura/cli
```

Check that it runs:

```bash
poltura --help
```

### Create the app [#create-the-app]

```bash
poltura app create my-app --template hosted
```

```text
created hosted app in /Users/you/my-app
```

`--template native` writes the UI workers only. `--template hosted` also writes `src/server.ts`, a small Node
server: `/connect` starts the connection, `/oauth/callback` finishes it and reads the store, and `/webhooks/products`
receives signed webhooks. The directory must be empty or not exist yet.

```text
my-app/
  poltura.app.json
  package.json
  tsconfig.json
  README.md
  src/
    elements.ts
    server.ts
    styles.css
    workers/
      admin-page.tsx
      storefront-home.tsx
```

### Install the dependencies [#install-the-dependencies]

```bash
cd my-app
npm install
```

The build needs `react`, `react-dom`, `@poltura/app-ui` and `tailwindcss` in the project. Without them it stops
with `Install react in this project to build workers.`

### Point the app at your server and your app id [#point-the-app-at-your-server-and-your-app-id]

`poltura.app.json` is the app's manifest. The scaffold uses `https://app.example.com` for every address; change
them to the HTTPS origin your server will run on, and add the app id from Developer apps so publishing knows
which app the release belongs to:

```jsonc title="poltura.app.json (excerpt)"
{
  "schemaVersion": 1,
  "apiVersion": "v1",
  "appId": "0199f3a2-5b1c-7d4e-9f20-3a4b5c6d7e8f", // [!code ++]
  "handle": "my-app",
  "releaseVersion": "1.0.0",
  "displayName": "My App",
  "backendOrigin": "https://app.example.com", // [!code --]
  "backendOrigin": "https://my-app.example.org", // [!code ++]
  "redirectUrls": ["https://app.example.com/oauth/callback"], // [!code --]
  "redirectUrls": ["https://my-app.example.org/oauth/callback"], // [!code ++]
}
```

Change `supportUrl`, `privacyUrl`, `homepageUrl` and the webhook and destination URLs the same way. The `handle`
must be the **Handle** you gave the app in Developer apps.

The permissions block lists what the app reads and writes. Every permission carries a purpose of 8 to 280 characters, and
merchants read it on the install screen before they approve:

```json title="poltura.app.json"
"permissions": {
  "required": [
    { "id": "settings.read", "purpose": "Read store locale, currency, and public capabilities" },
    { "id": "products.read", "purpose": "List products and collections for the companion admin page" }
  ],
  "optional": []
}
```

Keep only what the app uses. Installation storage needs no permission. Every permission is listed in
[Permissions and topics](/apps/permissions).

### Change the storefront block [#change-the-storefront-block]

The home block is declared once in `src/elements.ts`. Its settings become fields merchants edit in the
customizer, and its `style` list decides which style controls they get:

```ts title="src/elements.ts"
export const catalogHighlight = defineBlock({
  type: 'catalog-highlight',
  label: 'Catalog highlight',
  description: 'A short catalogue note on the home page.',
  icon: 'sparkles',
  category: 'marketing',
  settings: {
    title: field.text({ label: 'Heading', default: 'Catalog companion' }), // [!code --]
    title: field.text({ label: 'Heading', default: 'New this week' }), // [!code ++]
    note: field.text({ label: 'Note', default: 'Fresh picks from the catalogue.' }), // [!code ++]
    show_note: field.checkbox({ label: 'Show note', default: true }),
  },
  style: ['spacing', 'background', 'border'],
});
```

The worker draws it with the storefront primitives, so it follows the store's theme:

```tsx title="src/workers/storefront-home.tsx"
function Home(): ReactNode {
  const settings = useSettings();
  return (
    <Card className='rounded-theme bg-surface text-foreground p-4'>
      <Stack>
        <Heading>{String(settings.title ?? 'Catalog companion')}</Heading>
        {settings.show_note === false ? null : <Text>Storefront home block for this Poltura app.</Text>} {/* [!code --] */}
        {settings.show_note === false ? null : <Text>{String(settings.note ?? '')}</Text>} {/* [!code ++] */}
      </Stack>
    </Card>
  );
}
```

Check what the app now defines:

```bash
poltura app elements
```

```text
catalog-highlight	Catalog highlight	3 settings
```

### Build and validate [#build-and-validate]

```bash
poltura app build
poltura app validate
```

```text
built my-app@1.0.0  sha256-dfw5wNzsE0rCJlRiYs68FfVqGAQWsH+I6tStN34IgYI=  2 workers
built my-app@1.0.0  sha256-dfw5wNzsE0rCJlRiYs68FfVqGAQWsH+I6tStN34IgYI=  2 workers
ok my-app@1.0.0
  manifest.json  3906 bytes
```

Your digest and size differ. `build` bundles each worker, compiles `src/styles.css` and writes the release to `dist/`
(`--out` picks another directory). `validate` builds into a temporary directory and checks the manifest the
review will see. While you edit, `poltura app dev` rebuilds on every change and prints `watch <digest>` when the
release changes.

### Publish for review [#publish-for-review]

```bash
poltura app publish --api "$POLTURA_API" --token "$POLTURA_APP_TOKEN"
```

```text
published 0199f3b0-1e2d-7a3c-8b4f-5c6d7e8f9a0b
```

`--api` is the Poltura API origin and is required; setting `POLTURA_API` works too. Its production address is
not published yet (see [Authentication](/apps/auth)). Add `--admin-origin <your admin URL>` and the CLI also prints the
review link. If you skipped `appId` in the manifest, pass `--app <app id>`; without it the CLI stops with
`missing --app <appId>`.

### Review and install [#review-and-install]

A Poltura reviewer who is not in your organization checks the exact release you uploaded. Once it is approved,
a store can install it:

1. Run `src/server.ts` on your HTTPS origin with the variables its README lists, and `npm start`.
2. Open `https://my-app.example.org/connect?installation=<installation id>` as the store's administrator. Poltura shows the
   permissions with your purposes; approve, and the server answers `Connected to <store name>.`

The app's admin page and home block are now in that store, and merchants place the block in the Customizer like
any other. Everything after publishing is on [Ship to merchants](/apps/ship).


## Next steps [#next-steps]

- [Manifest fields](/apps/manifest): Every field of poltura.app.json.
- [Admin pages](/apps/admin): Native worker pages and hosted pages in the admin.
- [Receive webhooks](/apps/webhooks): Verify signed deliveries and handle retries.
- [Make your first API call](/api/app/first-call): Get an App API token, then list products.
