# Getting started

Build, embed, and ship your first Orbit plugin — an external web app that runs inside the Orbit store dashboard and talks to the Orbit API with scoped OAuth tokens.

This guide walks you from an empty directory to a plugin page rendering inside the dashboard iframe. By the end you will have a Next.js app, the SDK installed, a manifest, an embed page that receives a live store context, and the headers required for iframe embedding.

## What an Orbit plugin is

An Orbit plugin is an **external, standalone web app** that you build and host yourself (Next.js, React, or any stack that serves HTTPS). Orbit does not run your code. Instead, the store dashboard embeds your app as an `<iframe>` and brokers a secure connection to it.

Three properties define a plugin:

- **Embedded, not installed into Orbit.** Your UI mounts inside the dashboard through an iframe. Communication between the dashboard and your app happens over `window.postMessage`, which the SDK wraps for you.
- **OAuth-scoped.** Your plugin requests a set of scopes in its manifest. The merchant consents to them at install time, and every API call your plugin makes is checked against the granted scopes.
- **Owns its own data.** Your plugin stores everything in its own database. It cannot read or write Orbit's database directly — all Orbit data flows through the public API.

A single plugin is installed on many stores. Each store grants its own consent and carries its own context, so you never hard-code a store identity.

## Architecture overview

There are two communication paths to understand.

```
                postMessage (token + storeId)
  ┌─────────────────────────┐   <iframe>   ┌──────────────────────────┐
  │  Orbit store dashboard  │ ───────────▶ │  Your plugin app (HTTPS) │
  │admin.myorbitcommerce.net│ ◀─────────── │  Next.js / React / ...   │
  └─────────────────────────┘  UI helpers  └────────────┬─────────────┘
                                                         │ Bearer token (store-scoped)
                                                         ▼
                                            ┌──────────────────────────┐
                                            │  Orbit API               │
                                            │  api.myorbitcommerce.net  │
                                            │  /v1/*  /graphql  /oauth/*│
                                            └──────────────────────────┘
```

- **Dashboard ⇄ your app (postMessage).** When the dashboard loads your iframe, it sends the current session token and `storeId` over `postMessage`. The SDK listens for these. Your app can also post UI requests back — toasts, navigation, modals, resize — through the same channel.
- **Your app → Orbit API.** With the token and store context in hand, the SDK calls the Orbit API on `https://api.myorbitcommerce.net`, attaching `Authorization: Bearer <token>` to each request. The public surfaces are REST at `/v1/*`, GraphQL at `/graphql`, and the OAuth token endpoints at `/oauth/*`. On `/v1/*` the store is derived from the token itself — the `x-store-id` header the SDK also sends is only meaningful for `/graphql`. Every REST response arrives wrapped in the standard envelope `{ statusCode, message, data, timestamp }` (the SDK unwraps `data` for you; see [Authentication](/guides/authentication)).

The store and token are **provided dynamically by the dashboard** at runtime. Never bake a `storeId` into an environment variable or constant — the same deployed plugin serves every store that installs it.

## Quick start

### 1. Create a Next.js app

```bash
npx create-next-app@latest my-orbit-plugin --typescript
cd my-orbit-plugin
```

### 2. Install the SDK

```bash
npm install @orbitcommerce/sdk
```

The SDK exposes the `OrbitClient` class, which handles the postMessage handshake, token refresh, and typed API calls.

### 3. Add a `plugin.json` manifest

The manifest declares your plugin's identity, where its UI mounts, and which scopes it needs. You will import this in the partner dashboard wizard when you submit (see [Publishing](/guides/publishing)).

Scopes follow a strict `<resource>:<action>` convention where the resource is **singular** and kebab-case — `product:read`, `order:read`, not `products:read`. Request only what you need; the merchant sees and consents to this exact list.

```json
{
  "name": "Order Sync",
  "pluginId": "order-sync",
  "shortDescription": "Sync orders to your fulfillment system",
  "description": "Order Sync mirrors new and updated orders from your Orbit store into an external fulfillment system, keeping statuses in step without manual data entry.",
  "version": "1.0.0",
  "category": "sales",
  "tags": ["orders", "fulfillment"],
  "extensionPoints": [
    {
      "target": "dashboard.menu",
      "url": "https://my-orbit-plugin.example.com/embed",
      "label": "Order Sync"
    }
  ],
  "oauth": {
    "scopes": ["product:read", "order:read", "order:list"]
  },
  "ui": {
    "menu": { "label": "Order Sync" }
  }
}
```

See the full list of available scopes in the [Scope Catalog](/scope-catalog), and the [Scopes guide](/guides/scopes) for how requesting and enforcement work.

### 4. Build the embed page

Create the page that the `extensionPoints[].url` points to. It constructs an `OrbitClient`, waits for the dashboard handshake with `ready()`, then reads the live store context.

```tsx
// app/embed/page.tsx
'use client';

import { useEffect, useState } from 'react';
import { OrbitClient } from '@orbitcommerce/sdk';

export default function EmbedPage() {
  const [storeId, setStoreId] = useState<string | null>(null);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const orbit = new OrbitClient();

    orbit
      .ready() // resolves once the token + storeId arrive via postMessage
      .then(() => {
        setStoreId(orbit.getStoreId());
      })
      .catch((e: unknown) => {
        setError(e instanceof Error ? e.message : 'Failed to initialise');
      });

    // remove the postMessage listener on unmount
    return () => orbit.destroy();
  }, []);

  if (error) return <p>{error}</p>;
  if (!storeId) return <p>Connecting to Orbit…</p>;

  return <p>Connected to store {storeId}</p>;
}
```

A few things to note:

- `new OrbitClient()` with no arguments is the correct iframe constructor. The token, `storeId`, and API URL all arrive over postMessage — you do not pass them in.
- `ready()` accepts an optional timeout in milliseconds (default `10000`). It rejects if the handshake does not complete in time, so always handle the rejection.
- Read context only **after** `ready()` resolves: `orbit.getStoreId()` and `orbit.getToken()`. Calling them before the handshake returns nothing useful. Your granted scopes live in the token claims — see [Scopes](/guides/scopes).
- Call `orbit.destroy()` when the component unmounts to detach the listener.

Once connected, you can make API calls — for example `orbit.query(...)` / `orbit.mutate(...)` for GraphQL, or typed REST clients like `orbit.orders` and `orbit.products`. See the [SDK guide](/guides/sdk) for the full surface.

### 5. Configure CSP and HTTPS for iframe embedding

The dashboard can only embed your app if it is served over **HTTPS** and sends a `Content-Security-Policy` header whose `frame-ancestors` directive allows Orbit's domains. Without this header, the browser refuses to render your page inside the dashboard iframe.

Add the header in `next.config.ts`:

```ts
// next.config.ts
import type { NextConfig } from 'next';

const FRAME_ANCESTORS =
  "frame-ancestors 'self' https://*.orbitcommerce.net https://*.myorbitcommerce.net http://localhost:*";

const nextConfig: NextConfig = {
  async headers() {
    return [
      {
        source: '/embed/:path*',
        headers: [
          {
            key: 'Content-Security-Policy',
            value: FRAME_ANCESTORS,
          },
        ],
      },
    ];
  },
};

export default nextConfig;
```

The `http://localhost:*` entry lets you test against a locally running dashboard during development. In production your plugin must be reachable over HTTPS — browsers will not embed an insecure origin alongside the dashboard's secure one.

### 6. Run and embed

Start your app, expose it over HTTPS (for local development, a tunnel such as an HTTPS dev proxy works), and point your manifest's `extensionPoints[].url` at the `/embed` route. When the dashboard loads the iframe, the handshake fires, `ready()` resolves, and you will see the connected store ID render.

## Next steps

- [Authentication](/guides/authentication) — session vs access tokens, the postMessage handshake, and refreshing tokens for backend or background work.
- [SDK reference](/guides/sdk) — the full `OrbitClient` surface: GraphQL queries, typed REST clients, billing, and the iframe UI helpers.
- [Scopes](/guides/scopes) — how to choose scopes, the singular naming convention, consent, and 401/403 enforcement. Browse the complete [Scope Catalog](/scope-catalog).
- [Webhooks](/guides/webhooks) — subscribe to topics like `order.created`, verify delivery signatures, and handle retries.
- [Testing your plugin](/guides/testing) — install it on a real store with an install link, before it is listed or reviewed.
- [Publishing](/guides/publishing) — the 4-step submission wizard, the review process, and getting listed on the marketplace.
- [API reference](/api-reference) — the complete REST and GraphQL surface.
