Next.js SDK
@florianjs/opaque-next integrates with Next.js via the instrumentation hook — a built-in Next.js feature that runs once when the server starts, before any requests are handled.
Requirements
- Next.js >= 14.0
- Node.js runtime (not Edge runtime — Ed25519 signing requires Node.js crypto)
Installation
npm install @florianjs/opaque-nextConfiguration
1. Enable the instrumentation hook
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
experimental: {
instrumentationHook: true,
},
};
export default nextConfig;Next.js 15+
In Next.js 15, instrumentationHook is stable and enabled by default. You can omit the experimental flag.
2. Create instrumentation.ts
Create instrumentation.ts at the project root (same level as app/ or pages/):
// instrumentation.ts
import { register } from "@florianjs/opaque-next";
export { register };That is all. @florianjs/opaque-next exports a register function that Next.js calls automatically when the server starts.
3. Set environment variables
Add to your deployment environment (Vercel, Railway, etc.):
OPAQUE_PRIVATE_KEY='{"kty":"OKP","crv":"Ed25519","d":"...","x":"..."}'
OPAQUE_VAULT_URL="https://vault.example.com"
OPAQUE_PROJECT="my-app"For local development, add to .env.local (this file is gitignored by Next.js by default):
# .env.local — not committed
OPAQUE_PRIVATE_KEY='{"kty":"OKP","crv":"Ed25519","d":"...","x":"..."}'
OPAQUE_VAULT_URL="http://localhost:4200"
OPAQUE_PROJECT="my-app"How it works
The register function exported by @florianjs/opaque-next:
export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
const secrets = await fetchSecrets({
vaultUrl: process.env.OPAQUE_VAULT_URL ?? "",
privateKey: process.env.OPAQUE_PRIVATE_KEY ?? "",
project: process.env.OPAQUE_PROJECT ?? "",
});
injectEnv(secrets, process.env as Record<string, string>);
}
}The NEXT_RUNTIME === 'nodejs' guard ensures the fetch only runs in the Node.js runtime, not in the Edge runtime (which lacks the WebCrypto APIs needed for Ed25519 signing). If your app uses Edge middleware, those routes will not have opaque secrets — keep Edge routes stateless.
Using secrets in route handlers
After register() runs, all vault secrets are in process.env:
// app/api/data/route.ts
import { Pool } from "pg";
// process.env.DATABASE_URL was injected by @florianjs/opaque-next at startup
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
export async function GET() {
const result = await pool.query("SELECT now()");
return Response.json(result.rows[0]);
}// lib/stripe.ts
import Stripe from "stripe";
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2024-11-20",
});Vercel deployment
On Vercel, add the three env vars in your project settings under Settings → Environment Variables. Vercel runs instrumentation.ts on each serverless function cold start.
# Using Vercel CLI
vercel env add OPAQUE_PRIVATE_KEY
vercel env add OPAQUE_VAULT_URL
vercel env add OPAQUE_PROJECTMultiple environments on Vercel
Use Vercel's environment scoping (Production / Preview / Development) combined with opaque's env parameter to manage per-environment secrets:
- Production builds →
NODE_ENV=production→ opaque fetchesenv=productionsecrets - Preview deployments →
NODE_ENV=preview→ opaque fetchesenv=previewsecrets
Troubleshooting
Secrets not available in route handlers:
- Verify
instrumentation.tsis at the project root (not insideapp/orsrc/) - Verify
instrumentationHook: trueis innext.config.ts(Next.js < 15) - Check the server startup logs for
opaque:prefixed errors
Build-time errors:
- opaque runs at runtime, not build time.
process.env.DATABASE_URLwill be undefined duringnext build— this is expected. Don't access opaque-managed secrets ingetStaticPropsor module-level code that runs at build time.
Edge runtime:
- The
registerfunction skips whenNEXT_RUNTIME !== 'nodejs'. Secrets are not available in Edge middleware or Edge API routes.