@classytic/erp-shell
v1.0.0
Published
Domain-neutral ERP admin shell — multi-branch dashboard chrome (sidebar, route guard, branch context, member management, permission engine) on Arc + Better Auth + Fluid. Shared across verticals (commerce, hotel, ...).
Readme
@classytic/erp-shell
Domain-neutral ERP admin shell — the reusable dashboard chrome for multi-branch businesses on Arc + Better Auth + Fluid. One shell, many verticals (commerce, hotel, …). It owns the interaction (sidebar, launcher, route guard, branch switching, member management, permission engine); each vertical injects its data + policy.
Subpaths
| Import | What | Server-safe? |
|---|---|---|
| @classytic/erp-shell/access | hasAny/hasAll, PermCtx, CapabilityFlags, route-access engine (buildRouteAccessIndex, isRouteAllowed) | ✅ pure |
| @classytic/erp-shell/manifest | UI manifest types (DashboardManifest, SidebarCategory, …) + launcher selectors + AppDefinition | ✅ pure |
| @classytic/erp-shell/route-guard | RouteGuard (page-level access control) | client |
| @classytic/erp-shell/shell | DashboardShell, useSidebarConfig | client |
| @classytic/erp-shell/apps-grid | AppsGrid (launcher) | client |
| @classytic/erp-shell/auth | ErpAuthProvider, useErpAuth, ErpAuthApi (DI for the Better Auth client) | client |
| @classytic/erp-shell/branch | BranchProvider, useBranch, useIsHeadOffice, getActiveBranchId | client |
| @classytic/erp-shell/members | BranchMembersPanel, MembersConfigProvider, useMembersConfig, MembersConfig | client |
Nothing here imports a domain SDK. Everything vertical-specific is injected.
Building a vertical (the whole contract)
A vertical's app implements four things; then it reuses 100% of the shell.
1. Inject the Better Auth client → ErpAuthApi
The Better Auth client can't be re-exported from a dts-emitting package, so the
app creates it and adapts it. The : ErpAuthApi annotation is a checked
contract — a missing hook/method is a compile error.
// app/(auth)/_lib/client.ts
import type { ErpAuthApi, ErpOrganizationApi } from "@classytic/erp-shell/auth";
export const authClient = createAuthClient({ /* baseURL, plugins, ac, roles */ });
export const { useSession, useListOrganizations, useActiveOrganization, useActiveMember } = authClient;
export const erpAuth: ErpAuthApi = {
useSession, useListOrganizations, useActiveOrganization, useActiveMember,
// one narrow cast: BA types method params (e.g. inviteMember role) as a
// config-derived union that isn't portably nameable.
organization: authClient.organization as ErpOrganizationApi,
};Mount it above the branch/member tree: <ErpAuthProvider value={erpAuth}>…</ErpAuthProvider>.
2. Provide MembersConfig
import { MembersConfigProvider, type MembersConfig } from "@classytic/erp-shell/members";
const config: MembersConfig = {
roles: [{ value: "branch_manager", label: "Branch Manager" }, /* … your roles */],
canManage, // your permission gate (boolean)
setMemberStatus: async (id, status) => { /* your domain API */ },
permissionMatrix: <YourPermissionMatrix />, // optional reference panel (slot)
renderInviteDialog: (props) => <YourInviteDialog {...props} />, // slot — own your form
};
// <MembersConfigProvider value={config}>…</MembersConfigProvider>renderInviteDialog and permissionMatrix are slots so the shell never
touches your form system or permission model.
3. Author a DashboardManifest
Nav data + branding. Role gates are your own predicates over PermCtx
(e.g. canCommerce). Feed it to <DashboardShell manifest={…}> and
<RouteGuard manifest={…}>.
4. Add erp-shell to transpilePackages (required)
erp-shell ships pre-built ESM with "use client". Next must transpile it:
// next.config.ts
transpilePackages: ["@classytic/fluid", "@classytic/erp-shell"],Peers
React 19, Next ≥14, @classytic/fluid, @tanstack/react-query,
@tanstack/react-table, better-auth, lucide-react, sonner, clsx,
tailwind-merge — all optional peers; a subpath only needs the peers it uses.
Consuming apps must expose shadcn primitives at @/components/ui/* (the same
model @classytic/fluid uses).
