Edge Runtime Notes
:::caution Pre-release This is a pre-1.0 library (v0.1.1) — API may change without notice. :::
The collector (createCollectHandler) is designed to run safely on Vercel Edge Functions, Cloudflare Workers, and Deno Deploy. It uses Web Crypto only — no Node.js Buffer, no node:crypto, no database drivers.
Web Crypto Only
The server module is declared in package.json with explicit runtime constraints:
// packages/journey-recorder/src/server/index.ts
// Edge-safe means Web Crypto + `TextEncoder` only: no `Buffer`, no
// `node:crypto`, no TCP database driver, no `runtime = "nodejs"` assumption.
This is enforced by:
- Build-time: The server exports are bundled by tsup with
platform: 'neutral', excluding Node.js builtins. - Runtime-time: The collect handler (
createCollectHandler) only usesSubtleCryptofor hashing and HMAC operations.
Vercel Edge Functions
Use runtime = "edge" in your Route Handler:
import { createCollectHandler } from "@next-story/journey-recorder/server";
const handler = createCollectHandler({
tokenSecrets: {
current: process.env.NS_TOKEN_SECRET!,
previous: process.env.NS_TOKEN_SECRET_PREVIOUS,
},
ipSalt: process.env.NS_IP_SALT!,
sink: {
async storeEvents(batch) {
// Send to a database via fetch or other I/O
await fetch("https://api.example.com/events", {
method: "POST",
body: JSON.stringify(batch.events),
});
},
},
});
export async function POST(request: NextRequest) {
return handler(request);
}
export const runtime = "edge"; // Vercel Edge Functions
Recommended Settings
export const config = {
regions: ["sin1"], // Optional: specify regions
maxDuration: 30, // Edge Functions support 30-second timeout
};
export const RECOMMENDED_MAX_DURATION = 30; // Exported by the library
export const RECOMMENDED_RUNTIME = "edge"; // Exported by the library
Cloudflare Workers
The handler works with Cloudflare Workers via the fetch API:
import { createCollectHandler } from "@next-story/journey-recorder/server";
import { NextRequest } from "next/server"; // Or use native Request
const handler = createCollectHandler({
tokenSecrets: { current: env.NS_TOKEN_SECRET },
ipSalt: env.NS_IP_SALT,
sink: {
async storeEvents(batch) {
await env.EVENTS_KV.put(
`events-${Date.now()}`,
JSON.stringify(batch.events)
);
},
},
});
export default {
async fetch(request: Request) {
return handler(new NextRequest(request));
},
};
Database Connectivity
The collector does not connect to databases directly. Instead, it provides events to a sink callback that you implement:
const handler = createCollectHandler({
// ... secrets ...
sink: {
async storeEvents(batch) {
// Option 1: Use fetch to call a separate API endpoint
await fetch("https://your-db-api/events", {
method: "POST",
body: JSON.stringify(batch.events),
});
// Option 2: Use Neon serverless driver
const client = new Client({
connectionString: env.DATABASE_URL,
});
await client.connect();
await client.query("INSERT INTO raw_event (...) VALUES (...)", [...]);
await client.end();
// Option 3: Queue for later processing
await env.EVENTS_QUEUE.send({ events: batch.events });
},
},
});
This separation allows the collector to remain light and portable while you choose where events land.
Rate Limiting Store
By default, the collector uses an in-memory InMemoryStore for rate limits. In production with Edge replication, use Upstash Redis:
import { UpstashStore } from "@next-story/journey-recorder/server";
const store = new UpstashStore({
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
url: process.env.UPSTASH_REDIS_REST_URL!,
});
const handler = createCollectHandler({
// ... secrets ...
store, // Replicated rate limit state
// ... sink ...
});
Upstash REST API is HTTP-based and works anywhere, including Edge Functions.
Size and Performance
The compiled collector bundle is ~15 KB (minified, gzipped). Token verification and IP hashing are fast crypto operations suitable for sub-100ms latency.
Latency breakdown (typical):
- Token verification: ~1 ms
- IP hashing: ~2 ms
- Rate limit checks: ~5 ms (memory), ~10-20 ms (Upstash)
- Sink call (to your API): 10-500 ms (depends on your backend)
- Total: 16-530 ms
Most of the time is spent in your sink implementation, not in the collector itself.
Timeouts
- Vercel Edge Functions: 30-second max execution time (sufficient for most cases).
- Cloudflare Workers: 30-second max execution time (standard).
- Deno Deploy: 60-second max execution time.
If your sink needs longer, consider queueing events instead of processing synchronously:
sink: {
async storeEvents(batch) {
// Queue immediately, process async
await env.EVENTS_QUEUE.send(batch);
// Return quickly (< 100 ms) instead of waiting for sink
},
}
Example: Queued Sink
// Collect handler: queue events
const handler = createCollectHandler({
// ... secrets ...
sink: {
async storeEvents(batch) {
await env.EVENTS_QUEUE.send(batch);
},
},
});
// Separate worker/cron job: process queue
export async function processQueue() {
const events = await env.EVENTS_QUEUE.receive();
for (const batch of events) {
const db = new Client({ connectionString: env.DATABASE_URL });
await db.connect();
await db.query("INSERT INTO raw_event (...) VALUES (...)", [...]);
await db.end();
}
}
This pattern keeps the latency-critical collect path fast while processing events asynchronously.