> ## Documentation Index
> Fetch the complete documentation index at: https://help.cartble.com/llms.txt
> Use this file to discover all available pages before exploring further.

# How to build a custom plugin for your Cartble admin

> Learn how to build a custom Cartble plugin that adds admin pages, sidebar navigation, and scoped Firestore data to your store without touching core code.

Cartble's plugin architecture is designed to let developers extend the admin with new functionality while keeping all custom code self-contained and maintainable. This guide walks you through building a custom plugin from scratch — covering directory structure, the Bridge pattern, plugin registration, platform-aware data access, and state management conventions. The `smart-price` plugin at `src/plugins/smart-price` is the gold-standard reference implementation to study as you work through this guide.

## Who this is for

This guide is written for developers who are comfortable with React, TypeScript, and Firestore. You should have access to the Cartble codebase and be familiar with how the admin shell is structured before building a plugin.

## Plugin directory structure

Every plugin lives in its own subdirectory under `src/plugins/`. All plugin-specific code — pages, hooks, services, types, and components — must be entirely self-contained within that directory. Nothing inside a plugin should import from another plugin's directory.

```
src/plugins/
└── your-plugin-name/
    ├── index.ts          # Plugin definition and registration export
    └── src/
        ├── pages/        # Full-page React components
        ├── components/   # Shared UI components used within the plugin
        ├── hooks/        # Custom React hooks
        ├── services/     # Firestore and API service functions
        ├── types/        # TypeScript interfaces and types
        └── utils/        # Helper utilities
```

## What a plugin can do

A registered plugin can:

* **Add sidebar menu items** — define one or more `adminApps` entries with icons, labels, and page components
* **Add full admin pages** — React components rendered inside the admin shell when a sidebar item is clicked
* **Inject UI slots** — render components in predefined positions like product table columns or checkout flows
* **React to platform hooks** — execute logic when events like `on-checkout-start` or `on-order-placed` fire
* **Read and write Firestore data** — scoped to your platform's collection path

## The Bridge pattern

Plugins should not pollute the main application's React context. Instead, wrap every exported page component with a Higher-Order Component (HOC) that provides the plugin's own React Query client and localized settings. The `smart-price` plugin calls this `withSmartPriceBridge`.

```ts theme={null}
// src/plugins/your-plugin/src/components/YourPluginBridge.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient();

export function withYourPluginBridge<P extends object>(
  Component: React.ComponentType<P>
): React.ComponentType<P> {
  return function BridgedComponent(props: P) {
    return (
      <QueryClientProvider client={queryClient}>
        <Component {...props} />
      </QueryClientProvider>
    );
  };
}
```

Apply `withYourPluginBridge` to every page component you register in `adminApps`. This ensures your plugin's server-state management is completely isolated from the main application.

## Plugin registration

Define and export your plugin as a `Plugin` object in your `index.ts`. The plugin registry reads this object to wire up sidebar navigation, slots, and hooks.

```ts theme={null}
// src/plugins/your-plugin/index.ts
import { Plugin } from '../core/types';
import { LayoutGrid, Settings } from 'lucide-react';
import dynamic from 'next/dynamic';
import { withYourPluginBridge } from './src/components/YourPluginBridge';

const YourPluginDashboard = dynamic(() => import('./src/pages/Dashboard'));
const YourPluginSettings = dynamic(() => import('./src/pages/Settings'));

export const YourPlugin: Plugin = {
  id: 'your-plugin',
  name: 'Your Plugin',
  description: 'A short description of what your plugin does.',
  version: '1.0.0',
  category: 'other',
  isNative: true,
  adminApps: [
    {
      id: 'your-plugin-dashboard',
      label: 'Dashboard',
      icon: LayoutGrid,
      component: withYourPluginBridge(YourPluginDashboard),
    },
    {
      id: 'your-plugin-settings',
      label: 'Settings',
      icon: Settings,
      component: withYourPluginBridge(YourPluginSettings),
    },
  ],
};

export default YourPlugin;
```

After creating this file, register your plugin in the main plugin registry at `src/core/plugin-registry.ts` so it is picked up by the `PluginProvider`.

## Accessing platform context

Never hardcode a `platformId` or build Firestore collection paths by hand. Always use the `usePlatform` hook to retrieve the current platform's ID and scoped settings at runtime.

```ts theme={null}
import { usePlatform } from '@/src/hooks/usePlatform';

export function useYourPluginData() {
  const { platformId, settings } = usePlatform();

  // Safe — platformId comes from context
  const collectionPath = `platforms/${platformId}/your_plugin_data`;

  // ...
}
```

<Warning>
  Never hardcode a `platformId` string, collection name, or global constant inside your plugin. Doing so will cause data collisions when multiple platforms share the same Cartble deployment. Always use `usePlatform()` to retrieve the scoped ID at runtime.
</Warning>

## Data storage in Firestore

Store all plugin data under the platform-scoped path. Follow these conventions:

| Data type                    | Firestore path                             |
| ---------------------------- | ------------------------------------------ |
| Products / primary assets    | `platforms/{platformId}/resources/`        |
| Orders / transactions / logs | `platforms/{platformId}/records/`          |
| Plugin-private data          | `platforms/{platformId}/your_plugin_data/` |

Use a descriptive collection name for your plugin's private data (e.g., `smart_price_data`, `your_plugin_data`) to avoid collisions with other plugins.

## State management

Use React Query for all server-side state inside your plugin. Always scope your query keys with `platformId` to prevent data from one platform leaking into another when a user switches accounts.

```ts theme={null}
import { useQuery } from '@tanstack/react-query';
import { usePlatform } from '@/src/hooks/usePlatform';

export function useYourPluginSettings() {
  const { platformId } = usePlatform();

  return useQuery({
    // platformId is always the first element of the key
    queryKey: [platformId, 'your-plugin', 'settings'],
    queryFn: () => yourPluginService.getSettings(platformId),
  });
}
```

## Reference implementation

Study the `smart-price` plugin at `src/plugins/smart-price` for a complete example that demonstrates every pattern described in this guide — the Bridge HOC (`SmartPriceBridge`), full `adminApps` registration, Firestore service abstraction, scoped React Query keys, and UI component reuse from `src/components/ui/`.

<Tip>
  Start by copying the directory structure of `src/plugins/smart-price` and replacing the domain logic. This gives you a working scaffold with all the correct wiring already in place.
</Tip>
