From cbeae2f545b73e5ea49fc5aa96d11f12d4a4cafd Mon Sep 17 00:00:00 2001 From: Danilo Campos Date: Thu, 2 Apr 2026 14:29:05 +0100 Subject: [PATCH 1/4] Add Cloudflare Workers runtime reference Standalone markdown reference covering env access patterns, nodejs_compat requirement, and request lifecycle/flushing for posthog-node on Workers. Published as a release asset so the wizard can fetch it at runtime. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/build-release.yml | 7 ++++ docs/cloudflare-workers.md | 52 +++++++++++++++++++++++++++++ scripts/build.js | 13 ++++++++ 3 files changed, 72 insertions(+) create mode 100644 docs/cloudflare-workers.md diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 43a254b..659f009 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -168,6 +168,13 @@ jobs: # Upload skill-menu.json (used by the wizard to discover available skills) echo "Uploading skill-menu.json..." gh release upload "$RELEASE_TAG" dist/skills/skill-menu.json --clobber + # Upload reference docs (used by the wizard for runtime-specific overrides) + for file in dist/skills/*.md; do + [ -f "$file" ] || continue + filename=$(basename "$file") + echo "Uploading $filename..." + gh release upload "$RELEASE_TAG" "$file" --clobber + done - name: Update latest tag env: diff --git a/docs/cloudflare-workers.md b/docs/cloudflare-workers.md new file mode 100644 index 0000000..3b8d8c9 --- /dev/null +++ b/docs/cloudflare-workers.md @@ -0,0 +1,52 @@ +# Cloudflare Workers Runtime Reference + +This project deploys to Cloudflare Workers. The following overrides apply to ALL server-side code. Client-side posthog-js running in the browser is unaffected. + +## Environment Variables + +Workers have NO `process.env` and NO runtime `import.meta.env`. Environment variables are bound to the execution context and accessed through the framework adapter: + +| Framework | Server-side env access | +|-----------|----------------------| +| React Router 7 | `context.cloudflare.env.VAR_NAME` (in loaders, actions, middleware) | +| SvelteKit | `platform.env.VAR_NAME` (in hooks and server routes) | +| Nuxt | `process.env` works via `unenv` polyfill — but prefer `useRuntimeConfig()` | +| Astro | `Astro.locals.runtime.env.VAR_NAME` (in SSR pages and API routes) | +| Hono / raw Workers | `env.VAR_NAME` from the fetch handler's `env` parameter | + +Define variables in `wrangler.toml` under `[vars]` (non-secret) or via `wrangler secret put` (secret). Do NOT use `VITE_PUBLIC_` prefixed names on the server — those are Vite build-time replacements for client code only. + +## Node.js Compatibility + +`posthog-node` requires Node.js built-ins (`buffer`, `events`, `stream`, etc.). Add the compatibility flag to `wrangler.toml`: + +```toml +compatibility_flags = ["nodejs_compat"] +``` + +Without this, the SDK will crash at import time with missing module errors. + +## Request Lifecycle and Event Flushing + +Workers isolates are short-lived. Configure `posthog-node` for immediate dispatch and non-blocking flush: + +```typescript +const posthog = new PostHog(apiKey, { + host, + flushAt: 1, + flushInterval: 0, +}); + +// ... capture events ... + +// Flush without blocking the response +ctx.waitUntil(posthog.shutdown()); +``` + +- Set `flushAt: 1` and `flushInterval: 0` — do NOT rely on batch defaults designed for long-running servers. +- Use `ctx.waitUntil(posthog.shutdown())` to flush after the response is sent. The `ctx` / `waitUntil` source depends on the framework: + - React Router 7: `context.cloudflare.ctx.waitUntil()` + - SvelteKit: `platform.context.waitUntil()` + - Astro: `Astro.locals.runtime.ctx.waitUntil()` + - Hono / raw Workers: `ctx.executionCtx.waitUntil()` or `c.executionCtx.waitUntil()` +- Create a new PostHog client per request — do NOT use a singleton. Workers may reuse globals across requests on the same isolate, but the shutdown/flush lifecycle makes per-request instantiation safer. diff --git a/scripts/build.js b/scripts/build.js index d4e6b4a..3d2da4f 100755 --- a/scripts/build.js +++ b/scripts/build.js @@ -250,6 +250,19 @@ async function main() { fs.writeFileSync(skillMenuPath, JSON.stringify(skillMenu, null, 2)); console.log(` ✓ skill-menu.json (${Object.keys(skillsByCategory).length} categories, ${skills.length} skills)`); + // Copy standalone reference docs to skills dir (uploaded as release assets) + const refsDir = path.join(repoRoot, 'docs'); + if (fs.existsSync(refsDir)) { + const refFiles = fs.readdirSync(refsDir).filter(f => f.endsWith('.md')); + if (refFiles.length > 0) { + console.log('\nCopying reference docs...'); + for (const file of refFiles) { + fs.copyFileSync(path.join(refsDir, file), path.join(skillsDir, file)); + console.log(` ✓ ${file}`); + } + } + } + // Create bundled archive console.log('\nCreating bundled archive...'); const bundlePath = path.join(distDir, 'skills-mcp-resources.zip'); From cedeabbc7d1624677884e0877bc4a39e5f985a17 Mon Sep 17 00:00:00 2001 From: Danilo Campos Date: Thu, 2 Apr 2026 15:09:06 +0100 Subject: [PATCH 2/4] Apply corrections from research --- docs/cloudflare-workers.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/cloudflare-workers.md b/docs/cloudflare-workers.md index 3b8d8c9..228359a 100644 --- a/docs/cloudflare-workers.md +++ b/docs/cloudflare-workers.md @@ -4,14 +4,15 @@ This project deploys to Cloudflare Workers. The following overrides apply to ALL ## Environment Variables -Workers have NO `process.env` and NO runtime `import.meta.env`. Environment variables are bound to the execution context and accessed through the framework adapter: +Workers historically had no `process.env`, but since April 2025 (compatibility date `2025-04-01`+), `process.env` is populated automatically when using the `nodejs_compat` flag. You can also use `import { env } from 'cloudflare:workers'` to access bindings from anywhere, including top-level scope. `import.meta.env` remains build-time only (Vite static replacement). The canonical framework-specific patterns are: | Framework | Server-side env access | |-----------|----------------------| | React Router 7 | `context.cloudflare.env.VAR_NAME` (in loaders, actions, middleware) | | SvelteKit | `platform.env.VAR_NAME` (in hooks and server routes) | -| Nuxt | `process.env` works via `unenv` polyfill — but prefer `useRuntimeConfig()` | -| Astro | `Astro.locals.runtime.env.VAR_NAME` (in SSR pages and API routes) | +| Nuxt | `process.env` works via `unenv` polyfill — but prefer `useRuntimeConfig(event)` (pass `event` explicitly on CF) | +| Astro 5 | `Astro.locals.runtime.env.VAR_NAME` (in SSR pages and API routes) | +| Astro 6+ | `import { env } from 'cloudflare:workers'` (direct import; `Astro.locals.runtime` was removed) | | Hono / raw Workers | `env.VAR_NAME` from the fetch handler's `env` parameter | Define variables in `wrangler.toml` under `[vars]` (non-secret) or via `wrangler secret put` (secret). Do NOT use `VITE_PUBLIC_` prefixed names on the server — those are Vite build-time replacements for client code only. @@ -28,7 +29,7 @@ Without this, the SDK will crash at import time with missing module errors. ## Request Lifecycle and Event Flushing -Workers isolates are short-lived. Configure `posthog-node` for immediate dispatch and non-blocking flush: +Workers isolates are stateless — they may be reused across requests on the same edge location, but have no guaranteed longevity. Configure `posthog-node` for immediate dispatch and non-blocking flush: ```typescript const posthog = new PostHog(apiKey, { @@ -46,7 +47,10 @@ ctx.waitUntil(posthog.shutdown()); - Set `flushAt: 1` and `flushInterval: 0` — do NOT rely on batch defaults designed for long-running servers. - Use `ctx.waitUntil(posthog.shutdown())` to flush after the response is sent. The `ctx` / `waitUntil` source depends on the framework: - React Router 7: `context.cloudflare.ctx.waitUntil()` - - SvelteKit: `platform.context.waitUntil()` - - Astro: `Astro.locals.runtime.ctx.waitUntil()` + - SvelteKit: `platform.ctx.waitUntil()` + - Astro 5: `Astro.locals.runtime.ctx.waitUntil()` + - Astro 6+: `Astro.locals.cfContext.waitUntil()` - Hono / raw Workers: `ctx.executionCtx.waitUntil()` or `c.executionCtx.waitUntil()` +- As a framework-agnostic alternative, you can `import { waitUntil } from 'cloudflare:workers'` and call it from anywhere. +- PostHog's `captureImmediate()` method returns a Promise that can be passed directly to `waitUntil()` for single-event capture without full shutdown. - Create a new PostHog client per request — do NOT use a singleton. Workers may reuse globals across requests on the same isolate, but the shutdown/flush lifecycle makes per-request instantiation safer. From 1e0a6ea58c27ce0418d8a350d76c017afed15e62 Mon Sep 17 00:00:00 2001 From: Danilo Campos Date: Thu, 2 Apr 2026 15:22:48 +0100 Subject: [PATCH 3/4] Further refinements --- docs/cloudflare-workers.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/cloudflare-workers.md b/docs/cloudflare-workers.md index 228359a..87c1b5c 100644 --- a/docs/cloudflare-workers.md +++ b/docs/cloudflare-workers.md @@ -10,7 +10,7 @@ Workers historically had no `process.env`, but since April 2025 (compatibility d |-----------|----------------------| | React Router 7 | `context.cloudflare.env.VAR_NAME` (in loaders, actions, middleware) | | SvelteKit | `platform.env.VAR_NAME` (in hooks and server routes) | -| Nuxt | `process.env` works via `unenv` polyfill — but prefer `useRuntimeConfig(event)` (pass `event` explicitly on CF) | +| Nuxt | `process.env` works via `unenv` polyfill — but **only inside event handlers**, not at top-level. Prefer `useRuntimeConfig(event)` (pass `event` explicitly on CF) | | Astro 5 | `Astro.locals.runtime.env.VAR_NAME` (in SSR pages and API routes) | | Astro 6+ | `import { env } from 'cloudflare:workers'` (direct import; `Astro.locals.runtime` was removed) | | Hono / raw Workers | `env.VAR_NAME` from the fetch handler's `env` parameter | @@ -19,14 +19,12 @@ Define variables in `wrangler.toml` under `[vars]` (non-secret) or via `wrangler ## Node.js Compatibility -`posthog-node` requires Node.js built-ins (`buffer`, `events`, `stream`, etc.). Add the compatibility flag to `wrangler.toml`: +`posthog-node` ships a dedicated `workerd` export that avoids Node.js built-ins — it does NOT require `nodejs_compat` on its own. However, other dependencies in your project may need it. If you see missing-module errors at import time, add the compatibility flag to `wrangler.toml`: ```toml compatibility_flags = ["nodejs_compat"] ``` -Without this, the SDK will crash at import time with missing module errors. - ## Request Lifecycle and Event Flushing Workers isolates are stateless — they may be reused across requests on the same edge location, but have no guaranteed longevity. Configure `posthog-node` for immediate dispatch and non-blocking flush: @@ -50,7 +48,8 @@ ctx.waitUntil(posthog.shutdown()); - SvelteKit: `platform.ctx.waitUntil()` - Astro 5: `Astro.locals.runtime.ctx.waitUntil()` - Astro 6+: `Astro.locals.cfContext.waitUntil()` - - Hono / raw Workers: `ctx.executionCtx.waitUntil()` or `c.executionCtx.waitUntil()` + - Hono: `c.executionCtx.waitUntil()` + - Raw Workers: `ctx.waitUntil()` (`ctx` IS the `ExecutionContext` directly) - As a framework-agnostic alternative, you can `import { waitUntil } from 'cloudflare:workers'` and call it from anywhere. - PostHog's `captureImmediate()` method returns a Promise that can be passed directly to `waitUntil()` for single-event capture without full shutdown. -- Create a new PostHog client per request — do NOT use a singleton. Workers may reuse globals across requests on the same isolate, but the shutdown/flush lifecycle makes per-request instantiation safer. +- Prefer creating a new PostHog client per request. Workers may reuse globals across requests on the same isolate, but the shutdown/flush lifecycle makes per-request instantiation safer. From 40ad306a5ac18045b3596c4abf3e5b51ac292745 Mon Sep 17 00:00:00 2001 From: Danilo Campos Date: Thu, 2 Apr 2026 16:01:28 +0100 Subject: [PATCH 4/4] Use the .com-sourced version --- docs/cloudflare-workers.md | 55 --------------------------------- scripts/build.js | 19 ++++++------ transformation-config/docs.yaml | 8 +++++ 3 files changed, 18 insertions(+), 64 deletions(-) delete mode 100644 docs/cloudflare-workers.md diff --git a/docs/cloudflare-workers.md b/docs/cloudflare-workers.md deleted file mode 100644 index 87c1b5c..0000000 --- a/docs/cloudflare-workers.md +++ /dev/null @@ -1,55 +0,0 @@ -# Cloudflare Workers Runtime Reference - -This project deploys to Cloudflare Workers. The following overrides apply to ALL server-side code. Client-side posthog-js running in the browser is unaffected. - -## Environment Variables - -Workers historically had no `process.env`, but since April 2025 (compatibility date `2025-04-01`+), `process.env` is populated automatically when using the `nodejs_compat` flag. You can also use `import { env } from 'cloudflare:workers'` to access bindings from anywhere, including top-level scope. `import.meta.env` remains build-time only (Vite static replacement). The canonical framework-specific patterns are: - -| Framework | Server-side env access | -|-----------|----------------------| -| React Router 7 | `context.cloudflare.env.VAR_NAME` (in loaders, actions, middleware) | -| SvelteKit | `platform.env.VAR_NAME` (in hooks and server routes) | -| Nuxt | `process.env` works via `unenv` polyfill — but **only inside event handlers**, not at top-level. Prefer `useRuntimeConfig(event)` (pass `event` explicitly on CF) | -| Astro 5 | `Astro.locals.runtime.env.VAR_NAME` (in SSR pages and API routes) | -| Astro 6+ | `import { env } from 'cloudflare:workers'` (direct import; `Astro.locals.runtime` was removed) | -| Hono / raw Workers | `env.VAR_NAME` from the fetch handler's `env` parameter | - -Define variables in `wrangler.toml` under `[vars]` (non-secret) or via `wrangler secret put` (secret). Do NOT use `VITE_PUBLIC_` prefixed names on the server — those are Vite build-time replacements for client code only. - -## Node.js Compatibility - -`posthog-node` ships a dedicated `workerd` export that avoids Node.js built-ins — it does NOT require `nodejs_compat` on its own. However, other dependencies in your project may need it. If you see missing-module errors at import time, add the compatibility flag to `wrangler.toml`: - -```toml -compatibility_flags = ["nodejs_compat"] -``` - -## Request Lifecycle and Event Flushing - -Workers isolates are stateless — they may be reused across requests on the same edge location, but have no guaranteed longevity. Configure `posthog-node` for immediate dispatch and non-blocking flush: - -```typescript -const posthog = new PostHog(apiKey, { - host, - flushAt: 1, - flushInterval: 0, -}); - -// ... capture events ... - -// Flush without blocking the response -ctx.waitUntil(posthog.shutdown()); -``` - -- Set `flushAt: 1` and `flushInterval: 0` — do NOT rely on batch defaults designed for long-running servers. -- Use `ctx.waitUntil(posthog.shutdown())` to flush after the response is sent. The `ctx` / `waitUntil` source depends on the framework: - - React Router 7: `context.cloudflare.ctx.waitUntil()` - - SvelteKit: `platform.ctx.waitUntil()` - - Astro 5: `Astro.locals.runtime.ctx.waitUntil()` - - Astro 6+: `Astro.locals.cfContext.waitUntil()` - - Hono: `c.executionCtx.waitUntil()` - - Raw Workers: `ctx.waitUntil()` (`ctx` IS the `ExecutionContext` directly) -- As a framework-agnostic alternative, you can `import { waitUntil } from 'cloudflare:workers'` and call it from anywhere. -- PostHog's `captureImmediate()` method returns a Promise that can be passed directly to `waitUntil()` for single-event capture without full shutdown. -- Prefer creating a new PostHog client per request. Workers may reuse globals across requests on the same isolate, but the shutdown/flush lifecycle makes per-request instantiation safer. diff --git a/scripts/build.js b/scripts/build.js index 3d2da4f..a618c1a 100755 --- a/scripts/build.js +++ b/scripts/build.js @@ -250,15 +250,16 @@ async function main() { fs.writeFileSync(skillMenuPath, JSON.stringify(skillMenu, null, 2)); console.log(` ✓ skill-menu.json (${Object.keys(skillsByCategory).length} categories, ${skills.length} skills)`); - // Copy standalone reference docs to skills dir (uploaded as release assets) - const refsDir = path.join(repoRoot, 'docs'); - if (fs.existsSync(refsDir)) { - const refFiles = fs.readdirSync(refsDir).filter(f => f.endsWith('.md')); - if (refFiles.length > 0) { - console.log('\nCopying reference docs...'); - for (const file of refFiles) { - fs.copyFileSync(path.join(refsDir, file), path.join(skillsDir, file)); - console.log(` ✓ ${file}`); + // Write fetched docs marked as release_asset to skills dir (uploaded as release assets) + const releaseAssetDocs = docEntries.filter(d => d.release_asset); + if (releaseAssetDocs.length > 0) { + console.log('\nWriting release-asset docs...'); + for (const doc of releaseAssetDocs) { + const content = docContents[doc.id]; + if (content) { + const filename = `${doc.id}.md`; + fs.writeFileSync(path.join(skillsDir, filename), content); + console.log(` ✓ ${filename} (${content.length} chars)`); } } } diff --git a/transformation-config/docs.yaml b/transformation-config/docs.yaml index e3818c8..1ca6752 100644 --- a/transformation-config/docs.yaml +++ b/transformation-config/docs.yaml @@ -8,3 +8,11 @@ docs: tags: [core, users, identity] urls: - https://posthog.com/docs/getting-started/identify-users.md + + - id: cloudflare-workers + display_name: Cloudflare Workers + description: Runtime-specific overrides for PostHog on Cloudflare Workers (env access, event flushing, Node.js compat) + tags: [runtime, cloudflare, workers, edge] + release_asset: true # Also write as standalone .md for wizard to fetch directly + urls: + - https://posthog.com/docs/libraries/cloudflare-workers.md