Integration walkthrough
This walkthrough builds a feed screen the way the showcase app
(apps/showcase — a fitness demo running against the live API) does it: install,
authenticate, wrap your app in the provider, render a feed, then post an activity and
react to one.
-
Install the SDKs.
Terminal window pnpm add @dropin/client @dropin/react @dropin/server -
Mint a token on your backend. The showcase app’s token route gates minting to a known roster (never an arbitrary caller-supplied id) and constructs a
DropInServerper request from server-only env vars:// app/api/token/route.ts — server only, never bundled into the browserimport { DropInServer } from '@dropin/server'export const dynamic = 'force-dynamic' // never cache a minted tokenexport async function GET(req: Request): Promise<Response> {const athlete = new URL(req.url).searchParams.get('athlete') ?? ''if (!isAthleteId(athlete)) {return Response.json({ error: 'unknown athlete' }, { status: 400 })}const server = new DropInServer({tenantId: env.tenantId,apiKey: env.apiKey,apiSecret: env.signingSecret,url: env.apiUrl,})const token = await server.createUserToken(athlete, { expiresIn: '1h' })return Response.json({ token })}See Authentication & tokens for why the token is minted offline and what claims it carries.
-
Wrap your app in the provider. The showcase’s
Providerscomponent reads the current athlete from its own session context, then handsDropInProvideratokenProviderthat calls the route above:app/providers.tsx 'use client'import type { ReactNode } from 'react'import { DropInProvider } from '@dropin/react'import { SessionProvider, useSession } from '../lib/session.js'function KeyedClient({ children }: { children: ReactNode }) {const { athleteId } = useSession()const url = process.env.NEXT_PUBLIC_DROPIN_API_URL!const apiKey = process.env.NEXT_PUBLIC_DROPIN_API_KEY!const tokenProvider = async () => {const res = await fetch(`/api/token?athlete=${encodeURIComponent(athleteId)}`)if (!res.ok) throw new Error('token mint failed')return (await res.json()).token as string}return (<DropInProvider key={athleteId} url={url} apiKey={apiKey} tokenProvider={tokenProvider}>{children}</DropInProvider>)}export function Providers({ children }: { children: ReactNode }) {return (<SessionProvider><KeyedClient>{children}</KeyedClient></SessionProvider>)}The
key={athleteId}remounts the provider (and its underlying client) whenever the signed-in user changes — that’s how the showcase’s athlete switcher works. -
Render a feed. The explore page reads a flat feed with
useFeed; the following page reads the current user’s aggregated timeline withuseTimeline. Both hand the same shape —activities,isLoading,error,hasNext,loadNext,refresh— to a sharedFeedList:// app/(demo)/explore/page.tsx'use client'import { useFeed } from '@dropin/react'import { EXPLORE_FEED } from '../../../lib/demo-athletes.js'import { FeedList } from '../../../components/FeedList.js'export default function ExplorePage() {const f = useFeed<Workout>(EXPLORE_FEED.group, EXPLORE_FEED.id)return (<FeedListactivities={f.activities} isLoading={f.isLoading} error={f.error}hasNext={f.hasNext} loadNext={f.loadNext} refresh={f.refresh}empty="No workouts yet — run the seed script."/>)}// app/(demo)/following/page.tsx'use client'import { useTimeline } from '@dropin/react'import { useSession } from '../../../lib/session.js'export default function FollowingPage() {const { athleteId } = useSession()const f = useTimeline<Workout>(athleteId)return (<FeedListactivities={f.activities} isLoading={f.isLoading} error={f.error}hasNext={f.hasNext} loadNext={f.loadNext} refresh={f.refresh}empty={<>Nothing here yet. Find athletes to follow →</>}/>)}FeedListitself is a thin state-machine over those five fields — loading skeleton, error-with-retry, empty state, or a list of cards with a “Load more” button wired toloadNext:components/FeedList.tsx export function FeedList({activities, isLoading, error, hasNext, loadNext, refresh, empty,}: FeedListProps) {if (error && activities.length === 0) {return <div className="card">Couldn't load — <button onClick={refresh}>retry</button></div>}if (isLoading && activities.length === 0) return <FeedSkeleton />if (activities.length === 0) return <div className="card">{empty}</div>return (<div>{activities.map((a) => <WorkoutCard key={a.id} activity={a} />)}{hasNext && <button onClick={loadNext} disabled={isLoading}>{isLoading ? 'Loading…' : 'Load more'}</button>}</div>)} -
Post an activity and react.
ComposeSheetposts throughuseFeedActionsagainst the signed-in user’s own feed;LikeButtonreacts throughuseReactions, seeded from the activity’s own denormalized counts so there’s no extra fetch:components/ComposeSheet.tsx 'use client'import { useFeedActions } from '@dropin/react'import { useSession } from '../lib/session.js'export function ComposeSheet() {const { athleteId } = useSession()const { addActivity } = useFeedActions<Workout>('user', athleteId)const [note, setNote] = useState('')const [durationMin, setDuration] = useState(30)const [sport, setSport] = useState<Sport>('run')const post = async () => {await addActivity({ verb: 'workout', object: `workout:${Date.now()}`, custom: { sport, durationMin, note } })setNote('')}return <button onClick={post}>Post</button>}components/LikeButton.tsx 'use client'import { useReactions } from '@dropin/react'export function LikeButton({activityId, initialCounts, initialOwn,}: {activityId: stringinitialCounts: Record<string, number>initialOwn: string[]}) {const { react, unreact, counts, ownReactions } = useReactions(activityId, initialCounts, initialOwn)const liked = ownReactions.includes('like')const count = counts.like ?? 0const toggle = () => (liked ? unreact('like') : react('like')).catch(() => { /* hook already rolled back */ })return (<button aria-pressed={liked} onClick={toggle} aria-label={liked ? 'Unlike' : 'Like'}>{liked ? '❤️' : '🤍'} {count}</button>)}react/unreactapply optimistically and roll back on failure — thecatchabove is there only to swallow the already-handled rejection.
Troubleshooting integration
Section titled “Troubleshooting integration”Next: see the guides for the full per-task reference behind each hook used above.