"use client"; import { AnimatePresence, LayoutGroup, MotionConfig, motion } from "motion/react"; import { ArrowLeftIcon, ArrowRightIcon, CheckIcon, ChevronDownIcon, ChevronRightIcon, CopyIcon, CornerDownLeftIcon, FileTextIcon, HashIcon, InfoIcon, MenuIcon, PencilIcon, SearchIcon, ThumbsDownIcon, ThumbsUpIcon, TriangleAlertIcon, XIcon, } from "lucide-react"; import { Fragment, type KeyboardEvent, type ReactNode, useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react"; import { Button } from "@/components/ui/button"; import { Kbd } from "@/components/ui/kbd"; import { cn } from "@/lib/utils"; export type DocsLang = "node" | "python" | "go" | "curl"; export type DocsBlock = | { type: "p"; text: string } | { type: "list"; items: string[] } | { type: "steps"; items: string[] } | { type: "callout"; tone: "info" | "warning"; title: string; text: string } | { type: "code"; samples: Partial>; title?: string } | { type: "table"; head: string[]; rows: string[][] }; export interface DocsSection { id: string; title: string; blocks: DocsBlock[]; } export interface DocsPage { id: string; title: string; group: string; lead: string; updated: string; minutes: number; sections: DocsSection[]; } export interface DocsSiteProps { product?: string; version?: string; pages?: DocsPage[]; initialPage?: string; dashboardHref?: string; editHref?: (pageId: string) => string; className?: string; } const defaultPages: DocsPage[] = [ { id: "introduction", title: "Introduction", group: "Getting started", lead: "Kestrel delivers events from your product to your customers' endpoints, with retries, signatures and a delivery log they can trust.", updated: "Oct 2", minutes: 2, sections: [ { id: "what-you-can-build", title: "What you can build", blocks: [ { type: "p", text: "Anything your customers would otherwise poll for. Teams use Kestrel to send order updates to warehouses, sync CRM records, trigger builds and page on-call when a job fails." }, { type: "list", items: ["Outgoing webhooks with per-customer endpoints", "A hosted portal where customers manage their own endpoints", "Delivery logs, replays and alerts without building them yourself"] }, ], }, { id: "how-it-fits", title: "How it fits together", blocks: [ { type: "p", text: "Your backend publishes an event once. Kestrel fans it out to every subscribed endpoint, signs each request, retries failures and records every attempt." }, ], }, ], }, { id: "quickstart", title: "Quickstart", group: "Getting started", lead: "Send your first webhook in about five minutes.", updated: "Oct 18", minutes: 5, sections: [ { id: "install", title: "Install the SDK", blocks: [ { type: "code", samples: { node: "npm install @kestrel/node", python: "pip install kestrel", go: "go get kestrel.dev/go/v2", curl: "# No install needed. Every SDK call maps to one HTTP request." }, }, ], }, { id: "send-event", title: "Send an event", blocks: [ { type: "p", text: "Publish an event with a type and a JSON payload. Every endpoint subscribed to that type receives it." }, { type: "code", samples: { node: `const kestrel = new Kestrel(process.env.KESTREL_KEY);\n\nawait kestrel.events.publish({\n type: "order.shipped",\n data: { orderId: "ord_9QX2", carrier: "Parcelwise" },\n});`, python: `kestrel = Kestrel(os.environ["KESTREL_KEY"])\n\nkestrel.events.publish(\n type="order.shipped",\n data={"order_id": "ord_9QX2", "carrier": "Parcelwise"},\n)`, go: `client := kestrel.New(os.Getenv("KESTREL_KEY"))\n\nclient.Events.Publish(ctx, kestrel.Event{\n\tType: "order.shipped",\n\tData: map[string]any{"orderId": "ord_9QX2"},\n})`, curl: `curl https://api.kestrel.dev/v2/events \\\n -H "Authorization: Bearer $KESTREL_KEY" \\\n -d type=order.shipped \\\n -d data[orderId]=ord_9QX2`, }, }, { type: "callout", tone: "info", title: "Use a test key first", text: "Keys starting with kst_test_ deliver to test endpoints only, so you can try this without touching customer traffic." }, ], }, ], }, { id: "authentication", title: "Authentication", group: "Getting started", lead: "Every API request is authenticated with a secret key from your dashboard.", updated: "Sep 12", minutes: 2, sections: [ { id: "api-keys", title: "API keys", blocks: [ { type: "table", head: ["Prefix", "Environment", "Can deliver to"], rows: [ ["kst_live_", "Production", "Live endpoints"], ["kst_test_", "Test", "Test endpoints and the local CLI"], ], }, { type: "callout", tone: "warning", title: "Keep secret keys on the server", text: "Anyone with a secret key can publish events as you. Never ship one in a browser bundle or a mobile app." }, ], }, ], }, { id: "signatures", title: "Verifying signatures", group: "Webhooks", lead: "Kestrel signs every request so your customers can check it came from you and wasn't changed on the way.", updated: "Oct 9", minutes: 3, sections: [ { id: "how-signing-works", title: "How signing works", blocks: [ { type: "p", text: "Each request carries a Kestrel-Signature header: a timestamp and an HMAC-SHA256 of the timestamp and raw body, keyed with the endpoint's signing secret." }, ], }, { id: "verify", title: "Verify in your handler", blocks: [ { type: "code", samples: { node: `const event = kestrel.webhooks.verify(\n rawBody,\n req.headers["kestrel-signature"],\n process.env.ENDPOINT_SECRET,\n);`, python: `event = kestrel.webhooks.verify(\n raw_body,\n request.headers["Kestrel-Signature"],\n os.environ["ENDPOINT_SECRET"],\n)`, go: `event, err := kestrel.Verify(body, r.Header.Get("Kestrel-Signature"), secret)`, }, }, ], }, ], }, { id: "retries", title: "Retries and dead letters", group: "Webhooks", lead: "When an endpoint fails, Kestrel retries with backoff, then parks the event in a dead-letter queue, so nothing is lost and one bad endpoint never holds up the rest.", updated: "Oct 22", minutes: 4, sections: [ { id: "how-retries-work", title: "How retries work", blocks: [ { type: "callout", tone: "info", title: "Changed in v2.4", text: "Events now move to the dead-letter queue after three failed attempts. Before v2.4 they were retried for up to 24 hours.", }, { type: "p", text: "A delivery fails when the endpoint answers with anything other than a 2xx, takes longer than 15 seconds, or can't be reached. Each retry waits longer than the last, with jitter so a recovering endpoint isn't hit by every event at once." }, { type: "table", head: ["Attempt", "Sent after", "If it fails"], rows: [ ["1", "Immediately", "Retry in about 1 minute"], ["2", "~1 minute", "Retry in about 10 minutes"], ["3", "~10 minutes", "Move to the dead-letter queue"], ], }, { type: "p", text: "An endpoint that answers 410 Gone is disabled straight away and its pending events are dead-lettered." }, ], }, { id: "dead-letter-queue", title: "The dead-letter queue", blocks: [ { type: "p", text: "Dead-lettered events keep their payload, every attempt and the last error for 7 days. List them per endpoint to see what went wrong." }, { type: "code", title: "List dead letters for an endpoint", samples: { node: `const parked = await kestrel.deadLetters.list({\n endpointId: "ep_8fK21",\n limit: 20,\n});\n\nfor (const letter of parked.data) {\n console.log(letter.eventId, letter.lastError);\n}`, python: `parked = kestrel.dead_letters.list(endpoint_id="ep_8fK21", limit=20)\n\nfor letter in parked.data:\n print(letter.event_id, letter.last_error)`, go: `parked, err := client.DeadLetters.List(ctx, &kestrel.DeadLetterParams{\n\tEndpointID: "ep_8fK21",\n\tLimit: 20,\n})`, curl: `curl "https://api.kestrel.dev/v2/dead_letters?endpoint_id=ep_8fK21&limit=20" \\\n -H "Authorization: Bearer $KESTREL_KEY"`, }, }, ], }, { id: "replaying", title: "Replaying events", blocks: [ { type: "steps", items: [ "Fix the endpoint, or wait until your customer confirms it's back.", "Open Deliveries, filter by Dead-lettered, and select the events to send again.", "Choose Replay. Each event is delivered once more and counted as a new attempt.", ], }, { type: "code", title: "Replay from code", samples: { node: `await kestrel.deadLetters.replay("dl_3Hc9Ve");`, python: `kestrel.dead_letters.replay("dl_3Hc9Ve")`, go: `err := client.DeadLetters.Replay(ctx, "dl_3Hc9Ve")`, curl: `curl -X POST https://api.kestrel.dev/v2/dead_letters/dl_3Hc9Ve/replay \\\n -H "Authorization: Bearer $KESTREL_KEY"`, }, }, { type: "callout", tone: "warning", title: "Replays can arrive out of order", text: "A replayed event may land after newer events for the same object. Compare the event's created timestamp before overwriting state.", }, ], }, { id: "limits", title: "Limits", blocks: [ { type: "table", head: ["Limit", "Value"], rows: [ ["Attempts per event", "3"], ["Request timeout", "15 seconds"], ["Dead-letter retention", "7 days"], ["Replay rate", "50 events/s per endpoint"], ], }, ], }, ], }, { id: "replay-guide", title: "Bulk replays", group: "Webhooks", lead: "Send a whole window of events again after an outage, without flooding the endpoint.", updated: "Oct 22", minutes: 3, sections: [ { id: "choose-window", title: "Choose a window", blocks: [ { type: "p", text: "Pick a start and end time and an optional event type. Kestrel shows how many events match before anything is sent." }, { type: "code", samples: { node: `await kestrel.replays.create({\n endpointId: "ep_8fK21",\n from: "2026-10-21T09:00:00Z",\n to: "2026-10-21T09:40:00Z",\n});`, curl: `curl https://api.kestrel.dev/v2/replays \\\n -H "Authorization: Bearer $KESTREL_KEY" \\\n -d endpoint_id=ep_8fK21 \\\n -d from=2026-10-21T09:00:00Z \\\n -d to=2026-10-21T09:40:00Z`, }, }, ], }, { id: "pacing", title: "Pacing", blocks: [{ type: "p", text: "Bulk replays are paced at the endpoint's replay rate and pause automatically if the endpoint starts failing again." }], }, ], }, { id: "events-api", title: "Events", group: "API reference", lead: "The Event object, and how to list and retrieve events.", updated: "Oct 14", minutes: 3, sections: [ { id: "event-object", title: "The Event object", blocks: [ { type: "table", head: ["Field", "Type", "Description"], rows: [ ["id", "string", "Unique ID, prefixed evt_"], ["type", "string", "Dot-separated name, e.g. order.shipped"], ["data", "object", "Your payload, up to 256 KB"], ["created", "timestamp", "When you published it"], ], }, ], }, { id: "list-events", title: "List events", blocks: [ { type: "code", samples: { node: `const events = await kestrel.events.list({ type: "order.shipped" });`, python: `events = kestrel.events.list(type="order.shipped")`, curl: `curl "https://api.kestrel.dev/v2/events?type=order.shipped" \\\n -H "Authorization: Bearer $KESTREL_KEY"`, }, }, ], }, ], }, { id: "deliveries-api", title: "Deliveries", group: "API reference", lead: "Every attempt to send an event to an endpoint, with status, timing and response.", updated: "Oct 14", minutes: 2, sections: [ { id: "delivery-object", title: "The Delivery object", blocks: [ { type: "table", head: ["Field", "Type", "Description"], rows: [ ["id", "string", "Unique ID, prefixed dlv_"], ["status", "enum", "succeeded, failed or dead_lettered"], ["attempt", "integer", "1 to 3"], ["duration_ms", "integer", "Time until the endpoint answered"], ], }, ], }, ], }, ]; const LANGS: { id: DocsLang; label: string }[] = [ { id: "node", label: "Node.js" }, { id: "python", label: "Python" }, { id: "go", label: "Go" }, { id: "curl", label: "cURL" }, ]; const navLinks = ["Guides", "API reference"] as const; const ease = [0.23, 1, 0.32, 1] as const; const spring = { type: "spring", stiffness: 520, damping: 42 } as const; const KEYWORDS = /^(const|let|var|await|async|for|of|in|new|return|import|from|if|else|func|err|nil|range|def|print|True|False|None|curl)$/; function highlight(code: string): ReactNode[] { const out: ReactNode[] = []; const re = /((?:\/\/|#).*$)|("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)|(\s-[A-Za-z]\b|\s--?[a-z-]+)|(\b\d[\d_.]*\b)|([A-Za-z_$][\w$]*)/gm; let last = 0; let k = 0; for (let m = re.exec(code); m; m = re.exec(code)) { if (m.index > last) out.push(code.slice(last, m.index)); const tok = m[0]; if (m[1] && (m[1].startsWith("//") || /(^|\s)#/.test(code.slice(Math.max(0, m.index - 1), m.index + 1)))) out.push({tok}); else if (m[2]) out.push({tok}); else if (m[3]) out.push({tok}); else if (m[4]) out.push({tok}); else if (m[5] && KEYWORDS.test(tok)) out.push({tok}); else if (m[5] && /^[A-Z]/.test(tok)) out.push({tok}); else if (m[5] && code[re.lastIndex] === "(") out.push({tok}); else out.push(tok); last = re.lastIndex; } if (last < code.length) out.push(code.slice(last)); return out; } function CodeTabs({ samples, title, lang, onLang }: { samples: Partial>; title?: string; lang: DocsLang; onLang: (l: DocsLang) => void }) { const available = LANGS.filter((l) => samples[l.id]); const current = samples[lang] ? lang : available[0].id; const [copied, setCopied] = useState(false); useEffect(() => { if (!copied) return; const id = window.setTimeout(() => setCopied(false), 1600); return () => window.clearTimeout(id); }, [copied]); const single = available.length === 1 && !title; return (
{title && {title}}
{single ? ( terminal ) : ( available.map((l) => ( )) )}
        {highlight(samples[current] ?? "")}
      
); } function Inline({ text }: { text: string }) { const parts = text.split(/(kst_live_|kst_test_|Kestrel-Signature|evt_|dlv_|order\.shipped|410 Gone|2xx)/g); return ( <> {parts.map((p, i) => i % 2 === 1 ? ( {p} ) : ( {p} ), )} ); } function Block({ block, lang, onLang }: { block: DocsBlock; lang: DocsLang; onLang: (l: DocsLang) => void }) { switch (block.type) { case "p": return (

); case "list": return ( ); case "steps": return (
    {block.items.map((it, i) => (
  1. {i + 1} {it}
  2. ))}
); case "callout": { const Icon = block.tone === "info" ? InfoIcon : TriangleAlertIcon; return (
{block.title}

); } case "code": return ; case "table": return (
{block.head.map((h) => ( ))} {block.rows.map((r) => ( {r.map((c, i) => ( ))} ))}
{h}
{c}
); } } interface SearchHit { id: string; page: DocsPage; section?: DocsSection; } function SearchDialog({ pages, onPick, onClose }: { pages: DocsPage[]; onPick: (page: string, section?: string) => void; onClose: () => void }) { const [query, setQuery] = useState(""); const [active, setActive] = useState(0); const inputRef = useRef(null); useEffect(() => inputRef.current?.focus(), []); const hits = useMemo(() => { const q = query.trim().toLowerCase(); const all: SearchHit[] = pages.flatMap((p) => [ { id: p.id, page: p }, ...p.sections.map((s) => ({ id: `${p.id}#${s.id}`, page: p, section: s })), ]); if (!q) return all.filter((h) => !h.section).slice(0, 8); const score = (h: SearchHit) => { const title = (h.section ? h.section.title : h.page.title).toLowerCase(); if (title.startsWith(q)) return 0; if (title.includes(q)) return 1; if (h.page.title.toLowerCase().includes(q)) return 2; const body = h.section ? h.section.blocks.map((b) => ("text" in b ? b.text : "")).join(" ") : h.page.lead; return body.toLowerCase().includes(q) ? 3 : 9; }; return all .map((h) => ({ h, s: score(h) })) .filter((x) => x.s < 9) .sort((a, b) => a.s - b.s) .slice(0, 10) .map((x) => x.h); }, [pages, query]); const mark = (text: string) => { const q = query.trim(); if (!q) return text; const i = text.toLowerCase().indexOf(q.toLowerCase()); if (i < 0) return text; return ( <> {text.slice(0, i)} {text.slice(i, i + q.length)} {text.slice(i + q.length)} ); }; return ( e.target === e.currentTarget && onClose()} transition={{ duration: 0.12 }} >
{ setQuery(e.target.value); setActive(0); }} onKeyDown={(e) => { if (e.key === "ArrowDown") { e.preventDefault(); setActive((a) => Math.min(hits.length - 1, a + 1)); } else if (e.key === "ArrowUp") { e.preventDefault(); setActive((a) => Math.max(0, a - 1)); } else if (e.key === "Enter" && hits[active]) { onPick(hits[active].page.id, hits[active].section?.id); } else if (e.key === "Escape") { e.stopPropagation(); onClose(); } }} placeholder="Search guides and API reference…" ref={inputRef} value={query} /> Esc
{hits.length === 0 ? (
Nothing for “{query}”. Try “retry”, “signature” or “replay”.
) : ( <> {!query &&
Pages
} {hits.map((h, i) => (
onPick(h.page.id, h.section?.id)} onMouseMove={() => setActive(i)} role="option" > {h.section ? : } {mark(h.section ? h.section.title : h.page.title)} {h.page.group} {h.section ? ` › ${h.page.title}` : ""} {i === active && }
))} )}
); } export function DocsSite({ product = "Kestrel", version = "v2.4", pages = defaultPages, initialPage = "retries", dashboardHref, editHref, className, }: DocsSiteProps) { const [pageId, setPageId] = useState(initialPage); const [lang, setLang] = useState("node"); const [activeSection, setActiveSection] = useState(null); const [search, setSearch] = useState(false); const [drawer, setDrawer] = useState(false); const [tocOpen, setTocOpen] = useState(false); const [feedback, setFeedback] = useState>({}); const [closedGroups, setClosedGroups] = useState>(() => new Set()); const scrollRef = useRef(null); const pendingSection = useRef(null); const page = pages.find((p) => p.id === pageId) ?? pages[0]; const index = pages.indexOf(page); const prev = pages[index - 1]; const next = pages[index + 1]; const groups = Array.from(new Set(pages.map((p) => p.group))); const scrollToSection = (id: string, smooth = true) => { const box = scrollRef.current; const el = box?.querySelector(`[data-section="${id}"]`); if (box && el) box.scrollTo({ top: el.offsetTop - 16, behavior: smooth ? "smooth" : "auto" }); }; const go = (id: string, section?: string) => { setSearch(false); setDrawer(false); setTocOpen(false); if (id === pageId) { if (section) scrollToSection(section); else scrollRef.current?.scrollTo({ top: 0, behavior: "smooth" }); return; } pendingSection.current = section ?? null; setPageId(id); }; // biome-ignore lint/correctness/useExhaustiveDependencies: runs once per page change useLayoutEffect(() => { const box = scrollRef.current; if (!box) return; if (pendingSection.current) { scrollToSection(pendingSection.current, false); pendingSection.current = null; } else box.scrollTop = 0; setActiveSection(page.sections[0]?.id ?? null); }, [pageId]); const onScroll = () => { const box = scrollRef.current; if (!box) return; let current = page.sections[0]?.id ?? null; for (const s of page.sections) { const el = box.querySelector(`[data-section="${s.id}"]`); if (el && el.offsetTop - 120 <= box.scrollTop) current = s.id; } if (box.scrollTop + box.clientHeight >= box.scrollHeight - 4) current = page.sections[page.sections.length - 1]?.id ?? current; if (current !== activeSection) setActiveSection(current); }; const onKeyDown = (e: KeyboardEvent) => { if (search) return; if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") { e.preventDefault(); setSearch(true); return; } const t = e.target as HTMLElement; if (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || e.metaKey || e.ctrlKey || e.altKey) return; if (e.key === "/") { e.preventDefault(); setSearch(true); } else if (e.key === "Escape") { setDrawer(false); setTocOpen(false); } else if (e.key === "[" && prev) go(prev.id); else if (e.key === "]" && next) go(next.id); }; const sidebar = ( ); const toc = (
    {page.sections.map((s) => { const on = s.id === activeSection; return (
  • {on && }
  • ); })}
); return (
{ e.preventDefault(); go("introduction"); }}> {product} Docs {version}
{dashboardHref && ( )}
{page.group} {page.title}

{page.title}

{page.lead}

Updated {page.updated} {page.minutes} min read
{page.sections.length > 1 && (
{tocOpen && (
{toc}
)}
)} {page.sections.map((s) => (

{s.title}

{s.blocks.map((b, i) => ( ))}
))}
{feedback[page.id] ? ( {feedback[page.id] === "yes" ? "Thanks! Glad it helped." : "Thanks. We read every one of these and fix the page."} ) : ( Was this page helpful? )} {editHref && ( Edit this page )}
{prev ? ( ) : ( )} {next && ( )}
{drawer && ( <> setDrawer(false)} />
{product} Docs {version}
{sidebar}
)}
{search && setSearch(false)} onPick={go} pages={pages} />}
); }