HatchedDocs
Guides

Edge runtimes

Run @hatched/sdk-js on Cloudflare Workers, Vercel Edge, Deno, and Bun — fetch overrides, AbortSignal, and the crypto caveat.

The SDK is written against native web standards — fetch, Response, AbortSignal, crypto.randomUUID — so it runs unmodified on every modern edge runtime.

Cloudflare Workers

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

export interface Env {
  HATCHED_API_KEY: string;
}

export default {
  async fetch(req: Request, env: Env): Promise<Response> {
    const hatched = new HatchedClient({ apiKey: env.HATCHED_API_KEY });
    const health = await hatched.health();
    return Response.json(health);
  },
};

The server-only guard passes because Workers don't have a window or document global.

Webhooks on Workers

WebhooksResource.verifySignature uses node:crypto, which does not run on Workers. For Workers, verify manually with Web Crypto. Read the signature and timestamp from their separate headers — X-Hatched-Signature (strip the sha256= prefix) and X-Hatched-Timestamp:

export default {
  async fetch(req: Request, env: { HATCHED_WEBHOOK_SECRET: string }) {
    const body = await req.text();
    const sigHeader = req.headers.get('x-hatched-signature') ?? '';
    const tsHeader = req.headers.get('x-hatched-timestamp') ?? '';
    const ok = await verify(body, sigHeader, tsHeader, env.HATCHED_WEBHOOK_SECRET);
    if (!ok) return new Response('invalid signature', { status: 400 });
    // ... handle the event, ack with 202
    return new Response(null, { status: 202 });
  },
};

async function verify(
  body: string,
  signatureHeader: string,
  timestampHeader: string,
  secret: string,
): Promise<boolean> {
  const ts = timestampHeader;
  const sig = signatureHeader.replace(/^sha256=/, '');
  if (!ts || !sig) return false;
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;

  const key = await crypto.subtle.importKey(
    'raw',
    new TextEncoder().encode(secret),
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign'],
  );
  const expected = await crypto.subtle.sign(
    'HMAC',
    key,
    new TextEncoder().encode(`${ts}.${body}`),
  );
  const expectedHex = Array.from(new Uint8Array(expected))
    .map((b) => b.toString(16).padStart(2, '0'))
    .join('');
  return timingSafeEqualHex(expectedHex, sig);
}

function timingSafeEqualHex(a: string, b: string): boolean {
  if (a.length !== b.length) return false;
  let diff = 0;
  for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
  return diff === 0;
}

Vercel Edge

// app/api/hatched/health/route.ts
export const runtime = 'edge';

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

export async function GET() {
  const hatched = new HatchedClient({ apiKey: process.env.HATCHED_API_KEY! });
  return Response.json(await hatched.health());
}

For webhook verification with node:crypto, switch to runtime = 'nodejs'. Everything else (reads, event sends, widget session mint) works on Edge.

Deno

import { HatchedClient } from 'npm:@hatched/sdk-js';

const hatched = new HatchedClient({ apiKey: Deno.env.get('HATCHED_API_KEY')! });
console.log(await hatched.health());

Bun

import { HatchedClient } from '@hatched/sdk-js';
const hatched = new HatchedClient({ apiKey: Bun.env.HATCHED_API_KEY! });

Custom fetch override

Pass your own fetch — useful for:

  • Preflighting every request through a telemetry hop
  • Forcing a specific outbound pool on Workers (fetch(input, { cf: {...} }))
  • Injecting retries via a shared HTTP client
const hatched = new HatchedClient({
  apiKey: env.HATCHED_API_KEY,
  fetch: async (input, init) => {
    const start = Date.now();
    const res = await fetch(input, init);
    metrics.record('hatched.http', Date.now() - start);
    return res;
  },
});

Cancellation

AbortSignal.any is used internally to combine your signal with the SDK timeout. If your runtime doesn't have AbortSignal.any (very old environments), the SDK falls back to a manual combinator — no action needed on your side.