Build a Loop app
What this is: how to build a first-party Loop app (one owned by Loop, served to Loop users) against the platform.
Who it’s for: anyone working on loop-health, apps/admin, the developer portal, the LoopBio clinician portal, or a future Loop-owned consumer app.
Time: an afternoon to wire OAuth + first call; rest depends on the app.
How first-party differs from third-party
Loop apps run the same OAuth flow as any partner app. Eating our own dog food keeps the spec honest. The differences:
| Concern | Third-party | First-party |
|---|---|---|
| OAuth client registration | Self-service via developer portal | Pre-registered by platform team |
| Auto-approve scopes | No (user always sees consent) | Yes, for scopes the platform deems implicit (openid profile email read:account) |
| BAA | Required for PHI scopes | Loop is its own covered entity |
| Hostnames | <your-app>.example.com | *.loop.health (varies per app) |
| Branding | Your brand on consent screen | Loop brand |
The auth flow + SDK usage are identical. If you can follow build a partner app, you can build a Loop app.
Standard stack
Loop apps converge on:
- Next.js 15 (App Router)
- Clerk for sign-in UI on the consumer side
@platform/sdk+@platform/sdk-<service>for platform calls@platform/next-route-handlersfor API route boilerplate (withAuth,respondJson,proxyToService)- Tailwind + shadcn-derived components for UI
- Vercel for hosting (apps live in
~/Dev/loop/loop-platform, separate repo)
Workflow: sign-in → access token
The “exchange Clerk session for Loop access token” step: identity’s /v1/oauth/authorize accepts an internal grant where the Clerk session is the user proof (configured per first-party client). The app server-side gets back a normal access token and stores it in the user’s session.
Route handler pattern
Loop apps put route handlers behind withAuth from @platform/next-route-handlers:
// app/api/biomarkers/route.ts
import { withAuth, respondJson } from "@platform/next-route-handlers";
import { createClinicalClient } from "@platform/sdk-clinical";
import { platformHost, SERVICE_NAMES } from "@platform/hosts";
export const GET = withAuth(async ({ token }) => {
const clinical = createClinicalClient({
baseUrl: platformHost({ service: SERVICE_NAMES.CLINICAL }),
accessToken: token,
});
const result = await clinical.listBiomarkers();
return respondJson(result);
});withAuth resolves the Clerk session, exchanges for a Loop access token, and attaches it. respondJson enforces the platform JSON error shape.
No raw fetch() to platform services from app code. The no-raw-platform-fetch convention check rejects PRs that do this.
Auth surfaces in a first-party app
- Sign-in — Clerk modal or hosted page. Loop apps use Clerk.
- Token exchange — happens server-side in
withAuthmiddleware. - Refresh — SDK handles automatically when access token expires.
- Sign-out — Clerk sign-out + platform
revokecall to invalidate the access token.
Consuming events
Most Loop apps don’t subscribe to events directly — they query services on user actions. When they need push notifications:
- In-app inbox: subscribe to
comms.inbox.message.created.v1through a server-sent-events endpoint (GET /v1/inbox/streamon services/comms). - Web push: register a subscription on services/comms; service pushes via the user’s browser push subscription.
- Webhook: only if the app is also acting as an OAuth client for its own webhook deliveries (rare for first-party).
Brand context
If your Loop app serves multiple brands (e.g., it’s used by both loop.health and loopbio users), determine the brand from the subdomain or the user’s primary brand and pass it via:
- Subdomain routing —
loop.health→brand_id=loopis set during sign-in. X-Brand-Idheader — for explicit overrides (admin tools).
Don’t pass brand from user-controlled input. See Brands and multi-tenancy.
Deployment
Loop apps deploy via Vercel (~/Dev/loop/loop-platform), not via SST. Service deploys (SST) are separate from app deploys (Vercel). They share the platform SDKs as the contract.
What’s available to you
The SDKs cover every domain:
@platform/sdk-clinical— biomarkers, protocols, red flags@platform/sdk-payments— charges, subscriptions, disputes@platform/sdk-membership— tiers, win-back@platform/sdk-comms— inbox, prefs, send@platform/sdk-affiliates— referrals, commissions@platform/sdk-content— peptides, stacks, goals@platform/sdk-community— feed, posts@platform/sdk-follows— social graph@platform/sdk-analytics— track events@platform/sdk-identity— connected apps, user profile- … and more — see SDK reference
Checklist for a Loop app
- Clerk sign-in wired with the right publishable key per stage
- Server-side token exchange for Loop access tokens
-
withAuthon every API route handler - Brand context resolved from subdomain or token, never user input
- All platform calls via
@platform/sdk-* - Errors normalized to
LoopError - Rate-limit handling honors
Retry-After - Convention check
no-raw-platform-fetchpasses - Vercel preview environment matches dev stage URLs
Related
- Build a partner app — the auth flow is the same
- Auth model
- SDK reference
- Connect with Loop