Skip to main content

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.

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.
// 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.
// 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.
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`;

  // ...
}
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.

Data storage in Firestore

Store all plugin data under the platform-scoped path. Follow these conventions:
Data typeFirestore path
Products / primary assetsplatforms/{platformId}/resources/
Orders / transactions / logsplatforms/{platformId}/records/
Plugin-private dataplatforms/{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.
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/.
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.