Build an admin UI
What this is: the path for shipping a new admin UI on top of Loop’s platform — internal staff admin, partner-owned admin, anything that needs to talk to platform services with structured auth + audit.
Who it’s for: internal teams porting from a legacy admin, partner engineers building their own admin on top of the Loop OAuth platform, future you adding a new admin surface.
Time: half a day to a deployable admin with 1–2 pages working end-to-end.
Before you start
Read these:
- Admin kit — what the kit provides, how pages compose
- Auth model — especially the platform-admin sign-in section
- Audit & PHI
Tooling you need:
- Node 22+, pnpm 9+
- AWS SSO logged in (
aws sso login --profile theloopway) - Doppler logged in (for Cloudflare API token via
platform-infraproject) - A WorkOS account with an AuthKit application registered
Step 1 — Scaffold the app
cd ~/Dev/loop/platform
cp -r apps/_template-admin apps/your-admin-name # or copy apps/platform-admin(If _template-admin doesn’t exist yet, copy apps/platform-admin and rename — the template is a clone target.)
Edit apps/your-admin-name/package.json:
{
"name": "@apps/your-admin-name",
"dependencies": {
"@platform/admin-kit": "workspace:*",
"@platform/sdk-identity": "workspace:*",
"@platform/sdk-content": "workspace:*",
"next": "^14.2.0",
"react": "^18.3.0"
}
}Pick the SDK packages your admin will actually call.
Step 2 — Wire the providers
apps/your-admin-name/src/components/providers.tsx:
"use client";
import { AdminProvider } from "@platform/admin-kit";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useMemo, useState } from "react";
import { SERVICE_BASE_URLS } from "../lib/config";
export function Providers({ children, session }: { children: ReactNode; session: AdminSession }) {
const [queryClient] = useState(() => new QueryClient());
return (
<QueryClientProvider client={queryClient}>
<AdminProvider session={session} baseUrls={SERVICE_BASE_URLS}>
{children}
</AdminProvider>
</QueryClientProvider>
);
}The session comes from the WorkOS round-trip in step 3. Until then, session can be null and <AdminPage> will gate the route.
Step 3 — WorkOS sign-in flow
Copy the four files from apps/platform-admin:
src/middleware.ts— checks session cookie on every/admin/*requestsrc/app/sign-in/route.ts— redirects to WorkOS AuthKit with state cookiesrc/app/callback/route.ts— exchanges code, resolves staff roles via services/identity, sets signed cookiesrc/app/sign-out/route.ts— clears cookie
Adjust the WORKOS_* env-var names if your admin needs its own client. Most admins should share the platform-admin client unless they’re explicitly partner-owned.
See the WorkOS setup runbook for the dashboard config + secret values.
Step 4 — A first page
apps/your-admin-name/src/app/things/page.tsx:
"use client";
import { AdminPage, DataTable, TableSkeleton, useResourceList } from "@platform/admin-kit";
export default function ThingsPage() {
const things = useResourceList("identity", { resource: "things" });
return (
<AdminPage title="Things" scope="admin:identity">
<DataTable
data={things.data ?? []}
columns={[
{ header: "Name", accessor: "name" },
{ header: "Created", accessor: "created_at", format: "date" },
]}
skeleton={<TableSkeleton rows={10} />}
/>
</AdminPage>
);
}That’s a complete page. Loading state, error boundary, auth gate, scope check, audit-row rendering on the detail view — all from the kit.
Step 5 — SST infra
apps/your-admin-name/infra.ts:
export function yourAdminSite() {
const hostname =
$app.stage === "prod"
? "your-admin.platform.loop.health"
: `your-admin.${$app.stage}.platform.loop.health`;
const workosClientId = new sst.Secret("YOUR_ADMIN_WORKOS_CLIENT_ID", "");
const workosApiKey = new sst.Secret("YOUR_ADMIN_WORKOS_API_KEY", "");
const sessionCookieSecret = new sst.Secret("YOUR_ADMIN_SESSION_COOKIE_SECRET", "");
return new sst.aws.Nextjs("your-admin", {
path: "apps/your-admin-name",
domain: {
name: hostname,
dns: sst.cloudflare.dns({ proxy: false }),
},
environment: {
NEXT_PUBLIC_STAGE: $app.stage,
WORKOS_CLIENT_ID: workosClientId.value,
WORKOS_API_KEY: workosApiKey.value,
WORKOS_REDIRECT_URI:
$app.stage === "prod"
? "https://your-admin.platform.loop.health/callback"
: $interpolate`https://your-admin.${$app.stage}.platform.loop.health/callback`,
SESSION_COOKIE_SECRET: sessionCookieSecret.value,
},
});
}Wire it into the root sst.config.ts alongside the other apps.
Step 6 — Set the secrets
# Generate cookie secret
openssl rand -base64 32 # paste below
# Set per-stage via Doppler-wrapped sst
doppler run --project platform-infra --config dev -- \
pnpm sst secret set YOUR_ADMIN_WORKOS_CLIENT_ID 'client_...' --stage dev
doppler run --project platform-infra --config dev -- \
pnpm sst secret set YOUR_ADMIN_WORKOS_API_KEY 'sk_test_...' --stage dev
doppler run --project platform-infra --config dev -- \
pnpm sst secret set YOUR_ADMIN_SESSION_COOKIE_SECRET '<paste cookie secret>' --stage devThe Doppler wrapper supplies CLOUDFLARE_API_TOKEN which SST needs for DNS.
Step 7 — Configure WorkOS
In the WorkOS dashboard, on your application:
- Redirects → Redirect URIs → add
https://your-admin.dev.platform.loop.health/callback - Authentication → Methods → enable at least one (Magic Auth is simplest)
See the WorkOS setup runbook for full steps.
Step 8 — Deploy + test
Every push to main auto-deploys dev. To deploy manually:
doppler run --project platform-infra --config dev -- pnpm sst deploy --stage devThen visit https://your-admin.dev.platform.loop.health — you should land on /sign-in, get redirected to WorkOS, complete Magic Auth, and arrive at your admin.
What you got for free
By following this path, your admin inherits:
- WorkOS staff session with signed HttpOnly cookie
- Bearer-token auth to any platform service via the SDK
- Brand-scoped data access via
useBrandContext - Inline audit log rendering on detail views
- Tailwind v3 + shadcn primitives + Loop brand tokens
- TanStack Query revalidation, optimistic updates, error boundaries
- Convention-check enforcement (no raw fetch, no inline styles, no legacy
@loop/*)
What you still need to think about
- Which services your admin talks to. Pick
@platform/sdk-*packages selectively. - Scopes. Every page that mutates should be wrapped in
<RequireScope scope="admin:X">. Staff who lack the scope see a friendly denial, not a 401 in the dev console. - Brand scoping. If your admin operates across brands, use
<BrandPicker>. If it’s brand-specific, hard-code the brand in the provider. - PHI awareness. Pages that surface PHI must use the audit-aware patterns described in Audit & PHI.
Related
- Admin kit — primitives reference
- Auth model
- WorkOS + platform-admin setup runbook
apps/platform-admin— the canary reference implementation