HatchedDocs
Guides

Next.js integration

Wire Hatched into a Next.js App Router app — server components, route handlers, widgets, and webhooks.

Next.js is the most common host for Hatched integrations. The SDK is server-only, so every call happens in a server component, a route handler, or middleware — never in a "use client" component.

1. Install and configure

pnpm add @hatched/sdk-js
# .env.local
HATCHED_API_KEY=hatch_test_...
HATCHED_WEBHOOK_SECRET=whsec_...

2. Shared client

// lib/hatched.ts
import { HatchedClient } from '@hatched/sdk-js';

export const hatched = new HatchedClient({
  apiKey: process.env.HATCHED_API_KEY!,
});

Importing this module from a client component will fail at build time — good, that's the point. Keep it under lib/ or server/ and let the bundler prevent misuse.

3. Server component reads

// app/buddy/page.tsx
import { hatched } from '@/lib/hatched';

export default async function BuddyPage({ params }: { params: { userId: string } }) {
  const buddies = await hatched.buddies.list({ userId: params.userId });
  return <BuddyList data={buddies.data} />;
}

4. Route handlers for writes

Mutations (events, coin earn/spend, widget session mint) go through route handlers. They run on the server with access to HATCHED_API_KEY.

// app/api/hatched/events/route.ts
import { hatched } from '@/lib/hatched';
import { ValidationError } from '@hatched/sdk-js';

export async function POST(req: Request) {
  const { userId, lessonId, score } = await req.json();
  try {
    const effects = await hatched.events.send({
      eventId: `lesson_${lessonId}_${userId}`,
      userId,
      type: 'lesson_completed',
      properties: { lessonId, score },
    });
    return Response.json(effects);
  } catch (err) {
    if (err instanceof ValidationError) {
      return Response.json({ error: err.details }, { status: 422 });
    }
    throw err;
  }
}

5. Widget session mint endpoint

Your browser widget calls this to get a short-lived token. Never expose your secret API key directly.

// app/api/hatched/session/route.ts
import { hatched } from '@/lib/hatched';
import { getServerSession } from '@/lib/auth';

export async function POST() {
  const user = await getServerSession();
  if (!user) return new Response('unauthorized', { status: 401 });

  const session = await hatched.widgetSessions.create({
    buddyId: user.buddyId,
    userId: user.id,
    scopes: ['buddy:read', 'buddy:interact'],
    ttlSeconds: 60 * 15,
  });
  return Response.json({ token: session.token, expiresAt: session.expiresAt });
}
// components/buddy-widget.tsx
'use client';
import { useEffect, useRef, useState } from 'react';

export function BuddyWidget() {
  const [token, setToken] = useState<string | null>(null);
  const mountRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    fetch('/api/hatched/session', { method: 'POST' })
      .then((r) => r.json())
      .then(({ token }) => setToken(token));
  }, []);

  useEffect(() => {
    if (token) (window as any).Hatched?.rescan();
  }, [token]);

  if (!token) return null;
  return <div ref={mountRef} data-hatched-widget="buddy" data-hatched-token={token} />;
}

6. Webhook handler

Raw body is critical for signature verification. In the App Router, req.text() preserves the raw bytes.

// app/api/hatched/webhooks/route.ts
import { WebhooksResource } from '@hatched/sdk-js';

export const runtime = 'nodejs'; // `verifySignature` uses node:crypto

export async function POST(req: Request) {
  const raw = await req.text();
  const signature = req.headers.get('hatched-signature') ?? '';

  const valid = WebhooksResource.verifySignature(
    raw,
    signature,
    process.env.HATCHED_WEBHOOK_SECRET!,
  );
  if (!valid) return new Response('invalid signature', { status: 400 });

  const event = JSON.parse(raw);
  // enqueue for background processing
  await handle(event);

  return new Response(null, { status: 202 });
}

7. Middleware gotcha

Next.js Middleware runs in the Edge runtime. @hatched/sdk-js works in Edge only with publishableKey (read endpoints). For secret-key writes, move the logic into a runtime = 'nodejs' route handler.

Project layout recap

app/
  api/
    hatched/
      events/route.ts        POST — ingest an event
      session/route.ts       POST — mint widget session token
      webhooks/route.ts      POST — receive webhook (runtime=nodejs)
  buddy/page.tsx             server component using hatched.buddies.*
components/
  buddy-widget.tsx           "use client" — mounts the widget
lib/
  hatched.ts                 shared HatchedClient instance