diff --git a/plugins/next/README.md b/plugins/next/README.md
new file mode 100644
index 0000000..d1e343b
--- /dev/null
+++ b/plugins/next/README.md
@@ -0,0 +1,138 @@
+# @rep-protocol/next
+
+Next.js plugin for the [Runtime Environment Protocol (REP)](https://github.com/RuachTech/rep). Injects REP environment variables during development without needing the Go gateway.
+
+In production, this plugin does nothing — the REP gateway handles variable injection.
+
+## Install
+
+```bash
+pnpm add @rep-protocol/next
+# or
+npm install @rep-protocol/next
+```
+
+Peer dependencies: `next >= 14`, `react >= 18`.
+
+## Setup
+
+### 1. Add `` to your root layout
+
+`RepScript` is a React Server Component that injects a `` injection.
+- The dev session-key endpoint has no rate limiting or single-use semantics — production deployments must use the REP gateway.
+
+## Troubleshooting
+
+### `OperationError: The operation failed for an operation-specific reason`
+
+Key mismatch between `RepScript` and the session-key route. This happens if the bundler creates separate module instances for each entry point. The fix (applied in v0.1.12+) uses `globalThis` with `Symbol.for` to ensure a single key store across all bundles. Update to the latest version.
+
+### `REPError: SENSITIVE variable "X" not found in payload`
+
+The variable isn't set in your `.env.local`. If it's optional, catch the error:
+
+```ts
+const getOptionalKey = (): Promise =>
+ rep.getSecure("OPTIONAL_KEY").catch(() => "");
+```
+
+### `404` on `/rep/session-key` during development
+
+Either the API route or the rewrite is missing. Ensure both are set up (steps 2 and 3 above).
+
+## License
+
+Apache-2.0
diff --git a/plugins/next/src/keys.ts b/plugins/next/src/keys.ts
index 2b1a920..2576701 100644
--- a/plugins/next/src/keys.ts
+++ b/plugins/next/src/keys.ts
@@ -1,26 +1,41 @@
import { generateKeys, type Keys } from './crypto.js';
/**
- * Module-scoped singleton for ephemeral cryptographic keys.
+ * Process-wide singleton for ephemeral cryptographic keys.
+ *
+ * Uses globalThis so that the singleton survives even when bundlers (tsup,
+ * Turbopack, webpack) duplicate the module across CJS entry points. This
+ * is the standard Next.js singleton pattern (same approach Prisma uses).
*
* In Next.js dev, the server is a long-running Node.js process, so keys
* persist across requests within the same server lifecycle. New keys are
* generated on server restart — matching the gateway's behavior.
*
- * RepScript and the session-key route handler both import this module,
+ * RepScript and the session-key route handler both call getOrCreateKeys(),
* ensuring they share the same keys (encryption key used for the blob
* matches the key returned by /rep/session-key).
*/
-let _keys: Keys | null = null;
+
+const GLOBAL_KEY = Symbol.for('__rep_next_keys__');
+
+function getGlobal(): { keys: Keys | null } {
+ const g = globalThis as unknown as Record;
+ if (!g[GLOBAL_KEY]) {
+ g[GLOBAL_KEY] = { keys: null };
+ }
+ return g[GLOBAL_KEY]!;
+}
export function getOrCreateKeys(): Keys {
- if (!_keys) {
- _keys = generateKeys();
+ const store = getGlobal();
+ if (!store.keys) {
+ store.keys = generateKeys();
}
- return _keys;
+ return store.keys;
}
/** @internal For testing only. */
export function _resetKeys(): void {
- _keys = null;
+ const store = getGlobal();
+ store.keys = null;
}