CDP Web SDK
Single-file TypeScript tracker for browsers. No build step, no dependencies.
Install
Copy cdp.ts into your app. A common spot for Next.js:
your-app/
└── lib/
└── cdp.ts ← paste here
Init (Next.js App Router)
app/layout.tsx:
'use client';
import { useEffect } from 'react';
import { cdp } from '@/lib/cdp';
export default function RootLayout({ children }: { children: React.ReactNode }) {
useEffect(() => {
cdp.init({
writeKey: process.env.NEXT_PUBLIC_CDP_WRITE_KEY!,
endpoint: process.env.NEXT_PUBLIC_CDP_ENDPOINT ?? 'http://localhost:3049',
autoPage: true, // fire `page` on every SPA route change
});
}, []);
return <html><body>{children}</body></html>;
}
.env.local:
NEXT_PUBLIC_CDP_WRITE_KEY=cdp_dev_writekey_1234567890
NEXT_PUBLIC_CDP_ENDPOINT=http://localhost:3049
(The dev key above is the one seeded by infra/migrations/000002_seed_dev.up.sql.)
Use
import { cdp } from '@/lib/cdp';
// On login
cdp.identify(user.id, { email: user.email, plan: user.plan });
// On a meaningful action
cdp.track('Checkout Completed', { revenue: 199, currency: 'USD' });
// Manual page call (skip if autoPage is on)
cdp.page('Pricing');
// On logout
cdp.reset();
Vite / Create React App
Identical — cdp.init(...) in your root component / main.tsx.
What gets sent
Every call POSTs to ${endpoint}/v1/<type> with this envelope:
{
"type": "track",
"messageId": "uuid-v4",
"anonymousId": "uuid-v4 from localStorage",
"userId": "from identify()",
"sentAt": "2026-05-25T03:14:15Z",
"context": { "library_name": "cdp-web", "user_agent": "..." },
"event": "Checkout Completed",
"properties": { "revenue": 199, "currency": "USD" }
}
Header: Authorization: Basic base64(<writeKey>:).
Payload is Segment-compatible: if you ever swap endpoints to Segment the same code works.
Things to know
anonymousIdis generated once and persisted inlocalStorageundercdp_anon. It survives across sessions.userIdis persisted incdp_uiduntil you callcdp.reset().fetchuseskeepalive: trueso events fire even when the page is unloading. NosendBeaconbecause we need theAuthorizationheader.- For SSR (Next.js Server Components, Remix loaders) skip the SDK — fire events from your API route or a server-side function instead.
CORS
The ingest service serves Access-Control-Allow-Origin: * so any origin
works in dev. Lock this down for production (configure a reverse proxy or
patch internal/middleware/middleware.go).
Production checklist
- Issue a per-workspace write key from the console (don't ship the dev key)
- Restrict CORS to known origins
- Front the ingest service with HTTPS (browser refuses mixed content)
- Set
NEXT_PUBLIC_CDP_ENDPOINTto the public URL