ConceptsAdmin kit

Admin kit

What this is: @platform/admin-kit — the primitives layer that every admin UI on Loop is built from. Components, hooks, theme tokens, auth context. Sits on top of @platform/sdk-*.

Who it’s for: anyone building an admin surface — internal teams porting from legacy admins, partners building their own admins on top of Loop’s SDKs, future you maintaining what’s already shipped.

What to read next: Auth model, Audit & PHI, Brands & multi-tenancy.

Why a shared kit

Admin panels rot. Without shared primitives, every page has its own loading state, its own error handling, its own data-fetching pattern, its own brand picker, its own audit viewer. You end up with five subtly-different admins across the platform — each one a re-derivation of the same wheel, all of them slightly worse than the last.

@platform/admin-kit cuts that off. Every admin page in the Loop ecosystem composes the same primitives. When we improve <DataTable>, every admin gets it. When the design tokens change, every admin re-skins. When <AuditLogPanel> learns a new column, it shows up everywhere.

What’s in the box

PrimitivePurpose
<AdminProvider>Root context — session, brand, base URLs, TanStack Query client
<AdminPage>Page wrapper — auth gate + <ErrorBoundary> + <Suspense> + layout chrome
<DataTable>Generic sortable / paginated table fed by a useResourceList hook
<ResourceForm>Zod-validated form with optimistic updates and structured error rendering
<ResourceDetail>Read view + edit/delete actions + inline <AuditLogPanel>
<AuditLogPanel>Generic audit-row viewer — works against any service’s audit endpoint
<BrandPicker>Brand selector backed by @platform/brands, filtered by staff RBAC
<RequireScope>Declarative scope gate — <RequireScope scope="admin:identity">...
<Skeleton> / <TableSkeleton> / <FormSkeleton>Loading placeholders
<ErrorPanel>Renders structured AppError from @platform/contracts
useResource(service, id)Single-resource fetch with revalidation
useResourceList(service, filters)Paginated list with sort/filter
useResourceMutation()Create/update/delete with cache invalidation and rollback
useAuditLog(service, resourceId)Audit trail for a resource
useBrandContext()Current brand + brand list
useStaffSession()Current WorkOS staff session

Plus the Tailwind v3 config + shadcn primitives (Button, Card, Dialog, Table, Form, Toast, ~25 more) and the Loop brand-token CSS layer that ties them together.

How a page is built

Every admin page in the Loop ecosystem looks roughly like this:

// apps/platform-admin/src/app/oauth-clients/page.tsx
"use client";
 
import { AdminPage, DataTable, useResourceList } from "@platform/admin-kit";
 
export default function OAuthClientsPage() {
  const clients = useResourceList("identity", { resource: "oauth-clients" });
 
  return (
    <AdminPage title="OAuth clients" scope="admin:identity">
      <DataTable
        data={clients.data}
        columns={[
          { header: "Name", accessor: "name" },
          { header: "Client ID", accessor: "client_id" },
          { header: "Type", accessor: "type" },
          { header: "Created", accessor: "created_at", format: "date" },
        ]}
        onRowClick={(c) => router.push(`/oauth-clients/${c.id}`)}
        skeleton={<TableSkeleton rows={10} />}
      />
    </AdminPage>
  );
}

That’s it. No fetch logic. No loading state. No error boundary. No auth gate. No brand filter wiring. The hook + the components handle it. Mutation pages are similar — useResourceMutation returns a mutate function plus optimistic state.

Hard rules (enforced)

Convention checks block CI if these are violated in any admin app:

  1. No raw fetch. Data access goes through @platform/sdk-* clients, and SDK calls go through admin-kit hooks. The convention check rejects fetch( in apps/platform-admin/ and packages/admin-kit/.
  2. No style={{}}. All styling via Tailwind classes. The convention check rejects inline styles.
  3. No legacy @loop/*. Only @platform/* packages.
  4. Every page wrapped in <AdminPage>. The convention check verifies every route’s default export uses it.
  5. Mutations render audit rows inline. Proves audit is real-time.
  6. Brand-aware by default. Pages that mutate must pass the current brand_id via useBrandContext().

These rules don’t exist to be pedantic. Each one prevents a specific failure mode we’ve seen in legacy admins (silent staleness, accidental cross-brand writes, drift between what the UI shows and what audit captures, etc.).

Auth

Admin pages run behind a WorkOS staff session. The detailed flow is in the auth model, but the short version:

  1. User visits admin.{stage}.platform.loop.health → middleware checks for session cookie
  2. No cookie → redirect to /sign-in → redirect to WorkOS AuthKit
  3. User picks an auth method (Magic Auth code today; SSO/passkey later)
  4. WorkOS redirects to /callback with an authorization code
  5. /callback route exchanges the code for a user profile + access token, signs a session cookie, redirects to /admin
  6. Subsequent requests carry the signed cookie; useStaffSession() reads it

The access token from WorkOS is also what useResource* passes as the bearer when calling platform services. No second auth dance.

How partners would consume this

@platform/admin-kit is a real npm package (private registry today, public roadmap). A partner building their own admin on top of the Loop OAuth platform could:

  1. Install @platform/admin-kit + the specific @platform/sdk-* packages they need
  2. Wrap their app in <AdminProvider> with their own client_id / WorkOS config
  3. Compose pages with the same primitives

They get the same auth flow, the same audit visibility, the same brand-scoping, the same error handling — without rewriting any of it. This is what “Connect with Loop” looks like at the SDK layer.

Absorption roadmap

PhaseWhatStatus
1apps/platform-admin canary (oauth-clients, content, audit)Done (LOO-1909)
2Absorb command-center/apps/ops-dashboard page-by-pagePending
3Absorb loop-platform/apps/admin (post services/content content surface)Pending
4Service-specific admin needs added as new pagesOngoing

The full plan is in docs/architecture/admin-kit-roadmap.md (internal).

Source ADRs

ADR-0037 (services structure), ADR-0052 (Connect with Loop), and the admin-kit-roadmap doc (LOO-1909).