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/graphqlEvery authenticated request sends:
| Header | Required | Purpose |
|---|---|---|
Authorization: Bearer <accessToken> | Yes | Authenticates the admin user |
x-tenant: <tenantUid> | Merchant scope | Selects the merchant tenant |
Content-Type: application/json | Yes | Identifies 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:
| Query | Use it for |
|---|---|
report__dashboardOverview | Orders, 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.
Recommended request flow
- Sign in with
identity__login. - Select a tenant from the returned memberships.
- Fetch the saved
MERCHANTlayout withx-tenant. - Fetch report data for the same tenant.
- Merge the saved layout with your local widget catalog and render the dashboard.
- 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.