A public, production-shaped Next.js reference for
@reopt-ai/data-sdk-client
and
@reopt-ai/data-sdk-server.
Use it alongside the reopt Data documentation to
see browser, server, proxy, consent, identity, and delayed-event patterns in a
complete application.
Arc Supply is a fictional workspace-goods store created for this example. Its products, accounts, orders, brand, and imagery are illustrative; no purchase is real and the assets must not be used to represent reopt or a real merchant.
- A public write key resolved on the server and passed through a narrow Client Component boundary.
- Server credentials isolated in a
server-onlymodule. - A first-party
/ingestproxy that shares browser and server identity. - Consent withdrawal, identity reset, automatic events, Web Vitals, and error capture.
- Fail-open analytics: missing configuration and delivery failures do not break the storefront.
- Playwright assertions against payloads built by the real SDK transport.
- A development outbox worker that preserves per-event device identity.
- Analysis-ready commerce properties and a synthetic multi-channel journey seed.
Requirements: Node.js 22.22.1 or newer and pnpm 10.30.1.
corepack enable
pnpm install
pnpm devOpen http://localhost:4100. Credentials are optional: without a write key, the SDK becomes a no-op and the shop keeps working.
To connect a project:
cp .env.example .env.local
# Fill in values issued for your reopt Data project.
pnpm dev| Variable | Purpose | Requirement | Browser-visible |
|---|---|---|---|
REOPT_DATA_BASE_URL |
reopt Data endpoint | Defaults to https://data.reopt.ai |
Indirectly |
REOPT_DATA_WRITE_KEY |
Public project write key | Optional; analytics fail open | Yes |
REOPT_DATA_CLIENT_ID |
Server API client identifier | Optional, paired with secret | No |
REOPT_DATA_CLIENT_SECRET |
Server API client secret | Optional, paired with client ID | Never |
REOPT_DATA_PROJECT_ID |
Project used by round-trip verification | Verification only | No |
BETTER_AUTH_SECRET |
Example session-signing secret | Production; at least 32 characters | No |
BETTER_AUTH_URL |
Public origin of this application | Production | Yes |
REOPT_DATA_EXAMPLE_DIAGNOSTICS |
Enables diagnostic UI and routes in production | Optional; defaults to false |
No |
Do not add NEXT_PUBLIC_ to server credentials. Production startup rejects an
unsafe auth URL, an unsafe reopt Data URL, or an incomplete server credential
pair. HTTP service URLs are accepted only for localhost development.
With reopt-data and this repository next to each other, run the complete local
loop from this repository:
pnpm dev:stackIt links the four Data SDK packages, performs an initial dependency-aware
build, keeps their dist/ output watched, starts reopt-data with dev:min,
and starts this app. If .reopt-local.json exists, its first project is loaded
into the child-process environment without printing credential values. The
command does not create or reset Data resources and does not run a cron drainer.
Use pnpm sdk:mode to inspect the active source, pnpm sdk:tarball for a
package-fidelity check, and pnpm sdk:npm before handing off or deploying this
standalone repository.
The storefront's live events share a low-cardinality vocabulary for category,
price band, currency, checkout mode, value band, and funnel stage. That makes a
real visit immediately useful in property-filtered funnels such as
product.viewed → cart.added → checkout.started → order.completed.
For acquisition and channel comparisons, preview the synthetic dataset first:
pnpm analytics:seedThe command is dry-run by default. To apply it, start the configured storefront in one terminal and replay the plan in another:
pnpm dev
# another terminal
pnpm analytics:seed -- --applyThe development storefront must have REOPT_DATA_WRITE_KEY configured and
diagnostics enabled. The command drives the real UI through isolated browser
contexts, so the browser SDK opens genuine sessions and server-confirmed orders
join them normally. It does not use a privileged data-insertion shortcut.
One run sends synthetic, non-PII analytics journeys across paid search, email,
paid social, organic search, referral, and direct traffic. Client-side journey
events carry the same demo_run_id and cohort label for correlation; filter
the first funnel step by either property to isolate a run or cohort. Each
journey gets a unique browser identity, starts with an attributable $pageview,
and intentionally drops visitors between commerce stages so funnel and
conversion-rate differences are visible. Re-running creates a new run; it
never resets or deletes existing project data. Use --base-url to target a
different development storefront.
flowchart LR
Env[Deployment configuration] --> Resolver[Public project resolver]
Request[Request] --> Resolver
Resolver --> Layout[Next.js layout]
Layout --> Browser[Browser SDK]
Browser -->|/ingest/api/track| Proxy[Next.js proxy]
Proxy --> Data[data.reopt.ai]
Request --> Server[Server SDK]
Secrets[server-only credentials] --> Server
Server --> Data
LocalOutbox[Development outbox] --> Node[Node SDK worker]
Node --> Data
proxy.ts seeds the browser-readable device cookie and exposes first-party
ingest. Server-side events resolve the same request identity. Delayed local
events record deviceId on each row so a worker batch can contain multiple
visitors safely.
Diagnostics are enabled automatically in development. The lazy SDK button,
payload and identity drawer, runtime switches, /lab, and /api/boom are all
absent from production unless the deployment explicitly sets
REOPT_DATA_EXAMPLE_DIAGNOSTICS=true. Enable that only for an isolated,
controlled example deployment because diagnostic payloads can contain visitor
event data.
| Route | Integration exercised |
|---|---|
/ |
Storefront and integration overview |
/products |
Query navigation and automatic page views |
/products/[slug] |
Manual page views, register(), normalization, and track() |
/cart |
Browser cart events |
/checkout |
Server Action and Route Handler conversion paths |
/orders |
Request-scoped orders and demo outbox rows |
/account |
identify() on sign-in and reset() on sign-out |
/guide |
Capability-to-source map and installed npm package versions |
/lab |
Diagnostics-only exceptions, Web Vitals, consent, and queue controls |
/debug/errors |
Every exception shape, with breadcrumbs and a failing route handler |
The diagnostics settings exercise automatic page-view ownership, exception capture, exception breadcrumbs, an external consent manager, tracing headers, missing bootstrap, missing write key, and debug logging.
Six buttons, one per shape the issue detail renders differently:
| Button | What it exercises |
|---|---|
| Throw TypeError | window.onerror, an in-app stack frame |
| Unhandled rejection | unhandledrejection, a different listener from onerror |
| Capture handled | captureException(error, { level: "warning", fingerprint }) — its own grouping |
| Throw a string | A non-Error throw the SDK has to synthesize an Error for |
| Throw a cause chain | cause three deep, which the detail shows as a chain |
| Trigger server 500 | /api/debug/error → onRequestError in instrumentation.ts |
Turn Exception capture on in the SDK settings first: four of the six throw
rather than report, and with capture off nothing reaches the server. Turn
Exception breadcrumbs on as well to send $exception_steps — every button
records a step before it acts, so whichever one throws carries the trail of the
clicks before it.
lib/reopt/feature-map.ts is the source of truth
for the in-app /guide page. Update it and this table together whenever an SDK
capability changes.
| Area | API | Reference |
|---|---|---|
| Browser | <ReoptProvider config bootstrap> |
app/layout.tsx · components/reopt/analytics-provider.tsx |
| Browser | <ReoptPageView /> / pageView() |
components/reopt/analytics-provider.tsx · components/reopt/manual-page-view.tsx |
| Browser | <ReoptWebVitals /> |
components/reopt/analytics-provider.tsx · components/reopt/web-vitals-table.tsx |
| Browser | normalizePath |
lib/reopt/normalize-path.ts |
| Browser | init({ properties }) / register() |
components/reopt/analytics-provider.tsx · components/reopt/manual-page-view.tsx |
| Browser | track() |
components/reopt/product-view-event.tsx · components/reopt/checkout-view-event.tsx · components/shop/add-to-cart.tsx · components/shop/cart-lines.tsx |
| Browser | identify() / reset() |
components/shop/account-panel.tsx |
| Browser | getDeviceId() |
components/shop/checkout-form.tsx |
| Browser | capture.exceptions / captureException() |
components/reopt/instrumentation-lab.tsx |
| Browser | captureException(error, { level, fingerprint }) |
components/reopt/error-lab.tsx |
| Browser | capture.exceptionSteps / addExceptionStep() |
components/reopt/error-lab.tsx |
| Browser | consent.persist:false / setConsent() |
components/reopt/consent-banner.tsx · app/api/consent/route.ts |
| Browser | config.fetch / config.observe |
lib/reopt/devtools.ts · components/reopt/diagnostic-analytics-provider.tsx · components/reopt/devtools-drawer.tsx |
| Browser | flush() / pauseTracking() / resumeTracking() |
components/reopt/instrumentation-lab.tsx |
| Server | createReopt({ writeKey, credentials, getProfileId }) |
lib/reopt/server.ts |
| Server | getBootstrap() |
app/layout.tsx |
| Server | getReopt().track() |
app/actions.ts · app/api/orders/route.ts |
| Server | createOnRequestError() |
instrumentation.ts · app/api/boom/route.ts |
| Proxy | reoptProxy({ writeKey: resolver, proxy: true }) |
proxy.ts |
| Node | createReoptNode() + identity.deviceId |
scripts/forward.ts · lib/shop/outbox.ts |
| Test | window.__reoptDevtools |
e2e/*.spec.ts |
| Test | ingest requestId → Query API requestId |
e2e/roundtrip.spec.ts · lib/reopt/scenarios.ts |
lib/reopt/tenants.ts is safe for proxy.ts because
it exposes only the public write key and endpoint. The separate
lib/reopt/credentials.ts imports server-only.
Replace the public adapter with a host-aware project store if your application
serves multiple projects; keep credentials behind the same server boundary.
Checkout records the request-verified identity with each demo outbox row.
During development, pnpm forward reads .reopt-example/outbox.json and sends
pending rows through createReoptNode().
The file is a teaching aid, not a production queue. Production keeps demo rows only in process memory so filesystem availability can never affect checkout. Replace the demo outbox with a durable database or queue before adopting this pattern in a real application.
The diagnostic test suite injects the devtool recorder as
ReoptClientConfig.fetch and ReoptClientConfig.observe. It inspects the
sanitized payload and enqueue-time lifecycle produced by the SDK and still
allows the request to reach ingest; it does not make a stubbed request look
successful. The status bar reports the profile (anonymous or signed in)
separately from the signed analytics session, without displaying either
identifier. The recorder and drawer live behind the diagnostic client boundary,
so the production default requests neither.
reopt-data.config.mjs at the root links this repository to its reopt-data
project, the way prisma.config.ts links a schema to a database. It was written
by reopt-data link and holds ids and paths only — keys stay in the environment:
export default defineConfig({
projectId: "…",
organizationId: "…",
events: {
file: "./reopt-data.events.json",
types: "./lib/reopt-events.d.ts",
},
sourcemaps: {
dir: ".next/static",
urlPrefix: "http://localhost:4100/_next/static",
},
});Every reopt-data command reads its --project-id, --file, --dir … from
here when the flag is absent, so the scripts below carry no ids.
reopt-data config show prints what resolved.
reopt-data.events.json declares the app's navigation, product, cart, checkout,
order, search, and diagnostic events — including the complete
product.viewed → cart.added → checkout.started → order.completed funnel —
with their display names, statuses, conversion flag, and low-cardinality rollup
properties. The file is the truth: CI pushes it, so the catalogue is bound to
the same commit as the code that emits the events, instead of waiting for
someone to open the dashboard.
pnpm events:verify # CI gate — exit 8 when the file and the server disagree
pnpm events:push # apply the file (metric-changing and expensive changes included)
pnpm events:types # regenerate lib/reopt-events.d.ts for a typed track()reopt-data.events.lock.json records the last sync so reopt-data event diff
can tell an edit in this file from an edit made in the console. An event removed
from the file is archived, never deleted.
A production stack names a minified chunk, which is not somewhere anyone can go
and fix. Two build steps make it readable, wired here as postbuild:
reopt-data sourcemap inject
reopt-data sourcemap upload --release "$VERCEL_GIT_COMMIT_SHA"Both come from @reopt-ai/data-cli;
the build directory and URL prefix come from the config. The upload reads each
chunk's trailing //# sourceMappingURL to find its map — not the chunk's
name plus .map, which is wrong under turbopack — and uploads it under the URL
a stack frame will actually name. Only maps the server does not already have
are sent.
sourcemap inject appends a content-derived id to each chunk and the SDK
stamps it onto frames, so the maps keep matching when the host, the base path,
or the preview URL changes. It is optional; skip it and matching falls back to
the URL.
This app is one project with its own build, so it uploads by URL. A deployment that renders many projects — a brand front end on each brand's own domain — uploads once for the whole bundle instead, keyed by path:
reopt-data sourcemap upload --path-prefix /_next/static --host-app studio-web --quietThe origin is the part that differs per brand, so it is the part the ref leaves
out. Each project then points at the host app in its settings. --quiet keeps
progress off the build log; the summary and any failure still print.
next.config.ts sets productionBrowserSourceMaps: true — without it the
production build emits no browser maps and there is nothing to upload. Note
that it also serves those maps publicly.
postbuild decides what to do from the environment
(scripts/upload-sourcemaps.mjs):
| Environment | What runs |
|---|---|
| no credentials | --dry-run — a clone still builds |
REOPT_DATA_ORG_KEY (or the platform key) |
a real upload |
…plus REOPT_DATA_DELETE_MAPS=1 |
a real upload, then --delete-after-upload |
REOPT_DATA_ASSET_PREFIX |
overrides the config's URL prefix |
Delete the maps after uploading them, in production. next.config.ts
serves them publicly, which means the deployment hands anyone the original
source next to the bundle that minified it. That is deliberate here — this app
exists to be read, and the devtool demo shows maps resolving — but it is not
what you want in your own deployment.
--delete-after-upload removes each .map the run actually stored. A map that
failed to upload is left alone: it is the only copy, and the next run needs it
to retry. A partial failure exits 6, so CI notices.
Setting REOPT_DATA_DELETE_MAPS=1 without credentials prints a warning and
still does nothing — a dry run stores nothing, so there is nothing to delete.
pnpm check # formatting, lint, types, current tree and Git history safety
pnpm e2e # development server, fail-open and integration contracts
pnpm e2e:production # secure default plus explicit diagnostic production run
pnpm e2e:roundtrip # optional browser → ingest → Query API verificationThe round-trip suite requires REOPT_DATA_WRITE_KEY,
REOPT_DATA_PROJECT_ID, REOPT_DATA_CLIENT_ID, and
REOPT_DATA_CLIENT_SECRET; otherwise it skips. To validate an existing
deployment, set SHOP_DEPLOYED_URL=https://... and run pnpm e2e:deployed.
This repository intentionally does not use GitHub Actions. Contributors run the required checks locally and report results in the pull request.
sora@example.com/shop-demo-1234jiwon@example.com/shop-demo-1234
The accounts and all commerce data are recreated after a process restart. This is an SDK integration reference, not a commerce starter: replace its in-memory authentication, carts, orders, and outbox with your own persistence, authorization, privacy, rate limiting, and operational controls.
The reopt-skills repository ships
two agent skills for the Data SDK. They install into Claude Code, Codex, Cursor,
and other agents through the skills CLI and treat this repository as the
assembled reference app — the place to see multi-tenant resolution, the ingest
proxy, consent, identity, and the devtool working together in one Next.js app.
npx skills add reopt-ai/reopt-skills/data-sdk-install # first install or upgrade
npx skills add reopt-ai/reopt-skills/data-sdk-review # read-only integration auditdata-sdk-installpins areopt/data-sdk-agent-rulesblock into the consumer project'sAGENTS.md, connects existing credentials (it never creates Data resources), wires the Next.js proxy / bootstrap / provider, and routes everything else to the installed package READMEs.data-sdk-reviewaudits an existing integration for credential boundaries, proxy and bootstrap behavior, identity, consent, delivery, and production devtool exposure without editing code.
Both skills are version-gated against the SDK floor recorded in COMPATIBILITY.md. When the skills point you here, start from the capability map and the patterns worth copying.
Read CONTRIBUTING.md before proposing a change, SECURITY.md before reporting a vulnerability, and CODE_OF_CONDUCT.md for community expectations.
This example is available under the MIT License.
