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-pluginThe prompts:
? Plugin name: My First Plugin
? Template: React + Vite
? Create new plugin or link existing? Create newThe 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.exampleTwo files drive everything:
whatalo.app.tsdefines your plugin's identity, pages, and permissionswhatalo.app.tomltells 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 withread:storeonly.adminUI.pages— each entry creates a sidebar item. The first page is the default.appUrlandwebhookUrl— the CLI overrides these with the tunnel URL duringwhatalo 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 = 5173plugin_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 loginYour browser opens. Confirm the device code shown in the terminal. On success:
✓ Logged in as [email protected]Step 6: Start the Dev Session
whatalo devThe 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 quitPress 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 validateThe validator checks:
whatalo.app.tomlis present and validwhatalo.app.tsmanifest is present- Required dependencies installed (
@whatalo/plugin-sdk,react,vite) - No hardcoded secrets in source files
.gitignorecovers.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 --forceThe 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 draftStep 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