StoreOSv0.24.0
Guides

Build a custom admin dashboard

Use StoreOS core-api GraphQL to build a custom merchant or platform admin dashboard in your own web app.

This guide shows how to build a dashboard UI outside the StoreOS console while using core-api for authentication, tenant-scoped reports, and per-user widget layouts.

Use this guide for an internal tool, a white-label admin app, or a custom operations portal. It is different from the storefront API, which is intended for shoppers.

Architecture

Your web app owns the dashboard UI. StoreOS owns authentication, tenant access, reporting data, and saved layout state.

The core-api GraphQL endpoint is:

https://core-api.storeos.dev/graphql

Every authenticated request sends:

HeaderRequiredPurpose
Authorization: Bearer <accessToken>YesAuthenticates the admin user
x-tenant: <tenantUid>Merchant scopeSelects the merchant tenant
Content-Type: application/jsonYesIdentifies the GraphQL request

Do not expose a system or server credential in browser code. Use the signed-in user's token, or call core-api through your own server-side session/BFF.

1. Sign in

Call identity__login and keep the returned access token in your app's session. The login input uses the admin user's email and password.

mutation IdentityLogin($input: LoginInput!) {
  identity__login(input: $input) {
    accessToken
    mustChangePassword
    systemRoles
    memberships {
      tenant
      roles
    }
  }
}
const response = await fetch("https://core-api.storeos.dev/graphql", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    query: `
      mutation IdentityLogin($input: LoginInput!) {
        identity__login(input: $input) {
          accessToken
          mustChangePassword
          systemRoles
          memberships { tenant roles }
        }
      }
    `,
    variables: {
      input: {
        email: "admin@example.com",
        password: "your-password",
      },
    },
  }),
});

const { data } = await response.json();
const session = data.identity__login;

The response includes the user's tenant memberships and roles. Let the user select a tenant only from memberships returned by core-api; never trust an arbitrary tenant ID supplied by the browser.

2. Load a merchant dashboard

For a merchant dashboard, send the selected tenant UID in x-tenant. The report__dashboardOverview query returns the summary cards commonly shown at the top of a dashboard.

query DashboardOverview {
  report__dashboardOverview {
    orders {
      total
      growth { percentage period }
    }
    revenue {
      total
      currency
      growth { percentage period }
    }
    customers {
      total
      growth { percentage period }
    }
    products {
      total
      newThisWeek
    }
  }
}
const overviewResponse = await fetch(
  "https://core-api.storeos.dev/graphql",
  {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${session.accessToken}`,
      "x-tenant": selectedTenantUid,
    },
    body: JSON.stringify({
      query: `
        query DashboardOverview {
          report__dashboardOverview {
            orders { total growth { percentage period } }
            revenue { total currency growth { percentage period } }
            customers { total growth { percentage period } }
            products { total newThisWeek }
          }
        }
      `,
    }),
  },
);

const { data: overview } = await overviewResponse.json();

Use the other report queries to add more panels:

QueryUse it for
report__dashboardOverviewOrders, revenue, customers, and products summary
report__detailedReport(from, to)Order status, revenue by month, order counts, and top products
report__stockReport(from, to)Stock totals, stock status, movements, and low-stock items
report__couponReport(from, to)Coupon redemptions and discount totals

Dates use YYYY-MM-DD. If dates are omitted, each report uses its documented default window.

3. Add custom widgets

A widget is an application-defined component. The API does not render widgets or validate your widget catalog; it stores the widget ID, visibility, and grid span for the signed-in user.

const widgetCatalog = [
  { id: "sales-summary", component: SalesSummary },
  { id: "orders-by-status", component: OrdersByStatus },
  { id: "low-stock", component: LowStock },
];

Render only IDs from your own catalog. Treat unknown IDs as unavailable so an old saved layout cannot break the dashboard.

4. Save and restore the layout

Use identity__myDashboardLayout when the dashboard opens. A missing layout (null) means your app should use its default order.

query MyDashboardLayout($scope: DashboardLayoutScope!) {
  identity__myDashboardLayout(scope: $scope) {
    scope
    tenant
    widgets {
      id
      visible
      span
    }
    updatedAt
  }
}

For a merchant dashboard, use MERCHANT and send x-tenant. For a platform admin dashboard, use ADMIN; admin layouts are not tied to a tenant.

Save changes with identity__upsertMyDashboardLayout:

mutation SaveDashboardLayout($input: UpsertDashboardLayoutInput!) {
  identity__upsertMyDashboardLayout(input: $input) {
    scope
    tenant
    widgets {
      id
      visible
      span
    }
    updatedAt
  }
}
const layoutInput = {
  scope: "MERCHANT",
  widgets: [
    { id: "sales-summary", visible: true, span: "FULL" },
    { id: "orders-by-status", visible: true, span: "HALF" },
    { id: "low-stock", visible: false, span: "HALF" },
  ],
};

id is limited to 64 characters, at most 40 widgets can be submitted, and duplicate IDs are removed by core-api. span is HALF or FULL; omitted spans default to HALF.

  1. Sign in with identity__login.
  2. Select a tenant from the returned memberships.
  3. Fetch the saved MERCHANT layout with x-tenant.
  4. Fetch report data for the same tenant.
  5. Merge the saved layout with your local widget catalog and render the dashboard.
  6. Debounce layout changes and save them with identity__upsertMyDashboardLayout.

Keep authorization in core-api. Hiding a widget in your UI is not a permission check; every query and mutation must be authenticated and tenant-scoped by the API.

Error handling

GraphQL can return an HTTP 200 response with an errors array. Always check both the HTTP status and the GraphQL payload before rendering data.

const payload = await response.json();

if (!response.ok || payload.errors?.length) {
  throw new Error(
    payload.errors?.[0]?.message ?? "The dashboard request failed",
  );
}

For a ready-made merchant console implementation, see the StoreOS web console's dashboard queries and widget layout components in the repository.

On this page