Getting Started

Build Your First Plugin

A complete end-to-end tutorial — from scaffolding to a running plugin with real store data in 15 minutes.

This tutorial builds a working plugin step by step. By the end you will have a plugin with two pages, live store context, a toast action, and a full deployment to the Developer Portal.

Step 1: Create the Plugin

Run the scaffolder:

npx create-whatalo-plugin

The prompts:

? Plugin name: My First Plugin
? Template: React + Vite
? Create new plugin or link existing? Create new

The scaffolder creates the project directory, installs dependencies, and registers a new plugin entry in the Developer Portal.

Step 2: Explore the Project Structure

my-first-plugin/
├── whatalo.app.ts          ← Plugin manifest — pages, permissions, webhooks
├── whatalo.app.toml        ← Project config — slug, build commands, port
├── package.json
├── vite.config.ts
├── server/
│   ├── auth.ts             ← Session-token verification for backend routes
│   ├── env.ts              ← Server environment parsing
│   └── index.ts            ← Backend routes + webhook receiver
├── src/
│   ├── app.tsx             ← Main component — page router
│   ├── lib/plugin-api.ts   ← Same-origin frontend helper for /api calls
│   ├── main.tsx            ← React entry point
│   ├── pages/
│   │   └── settings.tsx    ← Settings page component
│   ├── webhooks/
│   │   └── verify.ts       ← Webhook signature helper
│   └── components/
│       └── whatalo-ui/     ← Pre-built UI components
└── .env.example

Two files drive everything:

  • whatalo.app.ts defines your plugin's identity, pages, and permissions
  • whatalo.app.toml tells the CLI how to build and run it

Step 3: Review the Manifest

Open whatalo.app.ts. This is what the scaffolder generates:

import { defineApp } from "@whatalo/plugin-sdk/manifest";

export default defineApp({
  id: "my-first-plugin",
  name: "My First Plugin",
  description: "Plugin starter generated with create-whatalo-plugin.",
  version: "0.1.0",
  author: {
    name: "Your Team",
    email: "[email protected]",
  },
  permissions: ["read:store"],
  appUrl: "https://your-tunnel-url.com",
  webhookUrl: "https://your-tunnel-url.com/webhooks/whatalo",
  pricing: "free",
  category: "developer",
  adminUI: {
    pages: [
      {
        path: "dashboard",
        title: "Dashboard",
        icon: "puzzle",
        position: "sidebar",
      },
      {
        path: "settings",
        title: "Settings",
        navigationLabel: "Configuración",
        icon: "settings",
        position: "sidebar",
      },
    ],
  },
});

Key points:

  • id — the plugin's unique identifier. Immutable after registration.
  • permissions — what store data your plugin can access. Starts with read:store only.
  • adminUI.pages — each entry creates a sidebar item. The first page is the default.
  • appUrl and webhookUrl — the CLI overrides these with the tunnel URL during whatalo dev, so the iframe frontend and backend routes share one public origin.

Step 4: Review the TOML Config

Open whatalo.app.toml:

# whatalo.app.toml — Generated by `whatalo init`

[plugin]
name = "My First Plugin"
plugin_id = "plg_abc123..."
slug = "my-first-plugin"

[build]
dev_command = "pnpm dev"
build_command = "pnpm build"
output_dir = "dist"

[dev]
port = 5173

plugin_id is set automatically during scaffolding — it links this project to the plugin entry in the Developer Portal.

Step 5: Authenticate

cd my-first-plugin
whatalo login

Your browser opens. Confirm the device code shown in the terminal. On success:

✓ Logged in as [email protected]

Step 6: Start the Dev Session

whatalo dev

The CLI starts your local app server, creates a cloudflared tunnel, and registers a temporary session in the admin sidebar. Watch for output like:

✓ Authenticated as [email protected]
✓ Manifest loaded (2 admin pages)
✓ Local server running at http://localhost:5173
✓ Tunnel active at https://abc123.trycloudflare.com
✓ Development session active

  Plugin:  My First Plugin
  Store:   Mi Tienda Demo
  Local:   http://localhost:5173
  Tunnel:  https://abc123.trycloudflare.com

  Preview: https://admin.whatalo.com/store/abc/integrations/my-first-plugin

  Admin pages:
    › Dashboard  → .../integrations/my-first-plugin
    › Settings   → .../integrations/my-first-plugin/settings

  Press p to open preview in browser
  Press r to restart server
  Press q to quit

Press p — the admin opens with your plugin in the sidebar.

If you later add more permissions to whatalo.app.ts, restart whatalo dev before testing useWhataloData() or checking token scopes. Development installations read from granted_scopes, which are synced when the dev session registers.

Step 7: Understand the App Component

Open src/app.tsx. This is the main router:

import { useWhataloContext } from "@whatalo/plugin-sdk/bridge";
import { useThemeSync, useAutoResize } from "./components/whatalo-ui";
import { SettingsPage } from "./pages/settings";

export function App() {
  // Syncs dark/light theme from admin to this document root
  useThemeSync();
  // Reports content height to host so admin manages scroll
  useAutoResize();

  const context = useWhataloContext();

  // Wait for the App Bridge handshake to complete
  if (!context.isReady) {
    return null;
  }

  // Route to the correct page based on what the admin tells us
  switch (context.currentPage) {
    case "settings":
      return <SettingsPage />;
    case "dashboard":
    default:
      return <DashboardPage />;
  }
}

useWhataloContext() gives you the full context object including currentPage, storeName, user, and theme. The isReady flag goes true once the App Bridge handshake completes — always guard your render on this.

Step 8: Display Real Store Data

The Dashboard page in the template already shows store context. Let's look at the relevant part:

function DashboardPage() {
  const context = useWhataloContext();

  return (
    <Page>
      <PageHeader title="My Plugin" subtitle="Plugin starter ready for development">
        <Button variant="primary">Primary action</Button>
      </PageHeader>
      <Layout>
        <Layout.Section>
          <Card>
            <BlockStack gap="300">
              <Text as="h2" variant="headingMd">Store Context</Text>
              <Box padding="300" background="surface-active" borderRadius="200">
                <BlockStack gap="200">
                  <InlineStack align="space-between">
                    <Text variant="bodySm" color="subdued">Store</Text>
                    <Text variant="bodySm" fontWeight="semibold">{context.storeName}</Text>
                  </InlineStack>
                  <InlineStack align="space-between">
                    <Text variant="bodySm" color="subdued">User</Text>
                    <Text variant="bodySm" fontWeight="semibold">{context.user.email}</Text>
                  </InlineStack>
                  <InlineStack align="space-between">
                    <Text variant="bodySm" color="subdued">Theme</Text>
                    <Badge tone="info">{context.theme}</Badge>
                  </InlineStack>
                </BlockStack>
              </Box>
            </BlockStack>
          </Card>
        </Layout.Section>
      </Layout>
    </Page>
  );
}

This data comes directly from the admin with no API call needed — it is pushed to your plugin via the App Bridge as soon as the handshake completes.

Step 9: Trigger a Toast from Your Plugin

The useAppBridge() hook combines context and actions into one API. Add a toast to the Dashboard:

import { useAppBridge } from "@whatalo/plugin-sdk/bridge";

function DashboardPage() {
  const bridge = useAppBridge();

  async function handleSave() {
    // ... your save logic ...
    await bridge.toast.show("Settings saved!", { variant: "success" });
  }

  return (
    <Page>
      <PageHeader title="My Plugin" />
      <Layout>
        <Layout.Section>
          <Card>
            <BlockStack gap="300">
              <Text variant="bodyMd">Store: {bridge.storeName}</Text>
              <Button variant="primary" onClick={handleSave}>
                Save
              </Button>
            </BlockStack>
          </Card>
        </Layout.Section>
      </Layout>
    </Page>
  );
}

Click the button in the admin — a success toast appears at the top of the screen.

Step 10: Add Page Navigation

Pages are routed via currentPage, which matches the path in your manifest. The Settings page is already wired up. Navigate between pages using the sidebar — or trigger navigation from your plugin:

const bridge = useAppBridge();

// Navigate to the settings page
bridge.navigate(`/store/${bridge.storeId}/integrations/my-first-plugin/settings`);

Step 11: Edit the Manifest Live

Add a third page to whatalo.app.ts while whatalo dev is running:

adminUI: {
  pages: [
    { path: "dashboard", title: "Dashboard", icon: "puzzle", position: "sidebar" },
    { path: "settings", title: "Settings", icon: "settings", position: "sidebar" },
    { path: "reports", title: "Reports", icon: "chart-bar", position: "sidebar" },
  ],
},

Save the file. The CLI detects the change and syncs the manifest to the admin within a second. Refresh the admin — you now see three sidebar items.

Add the route handler in src/app.tsx:

switch (context.currentPage) {
  case "settings":
    return <SettingsPage />;
  case "reports":
    return <ReportsPage />;
  case "dashboard":
  default:
    return <DashboardPage />;
}

Step 12: Request More Permissions

If your plugin needs order data, update permissions in the manifest:

permissions: ["read:store", "read:orders", "read:customers"],

Permissions are reviewed when a store owner installs your plugin. Only request what you actually use.

Step 13: Validate Before Deploying

whatalo validate

The validator checks:

  • whatalo.app.toml is present and valid
  • whatalo.app.ts manifest is present
  • Required dependencies installed (@whatalo/plugin-sdk, react, vite)
  • No hardcoded secrets in source files
  • .gitignore covers .env, .whatalo/, node_modules/
  • TypeScript compiles without errors
  • Dev port is available

Clean output means you are ready to deploy:

  Whatalo Plugin Validator
  ========================

  Config
  ✓ whatalo.app.toml found and valid

  Manifest
  ✓ whatalo.app.ts manifest found

  Dependencies
  ✓ @whatalo/plugin-sdk installed (^0.0.1)
  ✓ react installed (^19.0.0)
  ✓ vite installed (^6.0.0)

  Security
  ✓ No secrets found in source files
  ✓ .gitignore covers all required entries

  TypeScript
  ✓ TypeScript compilation passes

  Port
  ✓ Port 5173 is available

✓ All 9 checks passed — ready to submit!

Step 14: Deploy

whatalo deploy --force

The CLI builds your project, reads the version from package.json, and uploads the manifest to the Developer Portal:

✓ Authenticated as [email protected]
✓ Config valid (whatalo.app.toml)
✓ Build succeeded

  Deploy Complete
  ───────────────
  Plugin     My First Plugin
  Version    0.1.0
  Status     draft

Step 15: Submit for Review

After deploying, open the Developer Portal dashboard and navigate to your plugin. From the plugin detail page, click Submit for Review. The Whatalo team reviews your plugin and approves or returns it with feedback.

Once approved, your plugin is listed in the marketplace.

What You Built

  • A React + Vite plugin with two pages (Dashboard + Settings)
  • Real store context displayed from the App Bridge
  • A toast action triggered from your plugin
  • A live dev session with a cloudflared tunnel
  • Validated and deployed to the Developer Portal

Next Steps

  • App Bridge Reference — all context fields, actions, and billing API
  • CLI Reference — full documentation for all 14 commands
  • Configuration — manifest fields, permission scopes, TOML options
  • Webhooks — receive real-time events from the store
  • Billing — charge for your plugin with subscription plans

On this page