Skip to content

Add a custom admin page

Use this guide to add a Store page to an existing app. It displays the trusted shop identity returned by a server loader. You need a running app installed on a development store.

Create admin/pages/store/actions.server.ts:

import { defineAdminLoader } from 'openshop/admin'
export const storeSummary = defineAdminLoader({
handler: async ({ shop, shopifyApp }) => ({ shop, shopifyApp }),
})

The loader receives the identity verified by OpenShop. Keep database access, provider credentials, and other server-only work in this file.

Create admin/pages/store/page.tsx:

import { defineAdminPage, useLoader } from 'openshop/admin'
import { storeSummary } from './actions.server.ts'
function Store() {
const summary = useLoader(storeSummary, undefined)
return (
<s-page heading="Store">
{summary.loading && <s-spinner accessibilityLabel="Loading store" />}
{summary.error && <s-banner tone="critical">{summary.error.message}</s-banner>}
{summary.data && <s-paragraph>{summary.data.shop}</s-paragraph>}
</s-page>
)
}
export default defineAdminPage({ title: 'Store', component: Store })

OpenShop rewrites the page-local loader import to a typed browser stub. The browser calls the loader with a fresh Shopify session token.

Add this option to the existing app.defineConfig() call in openshop.config.ts, keeping its flows and other settings:

experimental: {
customPages: {
navigation: [{ label: 'Store', path: '/store' }],
},
},

Restart the development command after adding the page files. Open the app from Shopify admin and select Store. You should see the current shop domain. Run pnpm run lint and pnpm run build to verify both types and browser/server boundaries before deployment.

This example is available to authenticated app users. For narrower access, add pageAccess and loader/action authorize policies as described in the authorization reference. When querying app-owned tables, filter by the trusted shop and app identity; Drizzle does not add those filters automatically.