KeyEnvKeyEnv
SDKs & Integrations

Next.js Integration

Use KeyEnv secrets securely in Next.js applications.

Next.js Integration

Integrate KeyEnv secrets into your Next.js application with proper security practices for server and client components.

Features

  • Load secrets at build time or runtime
  • Server Components and Server Actions support
  • API Routes and Route Handlers support
  • Middleware integration
  • Environment-aware configuration
  • Works with App Router and Pages Router
  • Vercel deployment ready

Installation

npm install @keyenv/node

Or with other package managers:

yarn add @keyenv/node
pnpm add @keyenv/node

Quick Start

The recommended approach is to load secrets once at application startup.

Create a secrets loader in your app:

// lib/secrets.ts
import { KeyEnv } from '@keyenv/node';

let secretsLoaded = false;

export async function loadSecrets() {
  if (secretsLoaded) return;

  const client = new KeyEnv({
    token: process.env.KEYENV_TOKEN!,
  });

  await client.loadEnv(
    process.env.KEYENV_PROJECT_ID!,
    process.env.NODE_ENV === 'production' ? 'production' : 'development'
  );

  secretsLoaded = true;
}

Then call it in your layout or specific routes:

// app/layout.tsx
import { loadSecrets } from '@/lib/secrets';

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  await loadSecrets();

  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}

next.config.js Integration

For build-time secret injection, use a custom webpack configuration:

// next.config.js
const { KeyEnv } = require('@keyenv/node');

/** @type {import('next').NextConfig} */
const nextConfig = {
  // Other config options...
};

// Load secrets at build time
async function loadBuildSecrets() {
  if (!process.env.KEYENV_TOKEN) {
    console.warn('KEYENV_TOKEN not set, skipping secret loading');
    return {};
  }

  const client = new KeyEnv({
    token: process.env.KEYENV_TOKEN,
  });

  const env = process.env.VERCEL_ENV || process.env.NODE_ENV;
  const environment = env === 'production' ? 'production' : 'development';

  return await client.exportSecretsAsObject(
    process.env.KEYENV_PROJECT_ID,
    environment
  );
}

module.exports = async () => {
  const secrets = await loadBuildSecrets();

  return {
    ...nextConfig,
    env: {
      ...secrets,
    },
  };
};

Build-time secrets are embedded in the bundle. Only use this for non-sensitive configuration. For sensitive secrets, load them at runtime in Server Components.


Server Components vs Client Components

Next.js has different execution contexts. Understanding where secrets are safe is critical.

ContextSecrets Safe?Access Method
Server ComponentsYesprocess.env.SECRET
Server ActionsYesprocess.env.SECRET
API Routes / Route HandlersYesprocess.env.SECRET
MiddlewareYesprocess.env.SECRET
Client ComponentsNoNever expose secrets

Server Components (Safe)

Server Components run only on the server. Secrets are never sent to the browser:

// app/dashboard/page.tsx
import { KeyEnv } from '@keyenv/node';

export default async function DashboardPage() {
  // Safe: This code only runs on the server
  const client = new KeyEnv({
    token: process.env.KEYENV_TOKEN!,
  });

  const secrets = await client.exportSecretsAsObject(
    process.env.KEYENV_PROJECT_ID!,
    'production'
  );

  // Use the secret to fetch data
  const data = await fetch('https://api.example.com/data', {
    headers: {
      Authorization: `Bearer ${secrets.API_KEY}`,
    },
  });

  return <div>{/* Render data, not secrets */}</div>;
}

Client Components (Never Use Secrets)

Client Components run in the browser. Never access secrets here:

// components/UserProfile.tsx
'use client';

// WRONG: Never do this in a client component
// const apiKey = process.env.API_KEY; // This would be undefined or leaked

// CORRECT: Fetch data through an API route instead
export function UserProfile() {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch('/api/user-data')
      .then(res => res.json())
      .then(setData);
  }, []);

  return <div>{/* Render data */}</div>;
}

Public Environment Variables

For non-sensitive values needed in the browser, use the NEXT_PUBLIC_ prefix:

# In KeyEnv, create these secrets:
NEXT_PUBLIC_APP_URL=https://myapp.com
NEXT_PUBLIC_ANALYTICS_ID=UA-123456789
// components/Analytics.tsx
'use client';

export function Analytics() {
  // Safe: These are intentionally public
  const analyticsId = process.env.NEXT_PUBLIC_ANALYTICS_ID;

  return <script data-analytics-id={analyticsId} />;
}

Only use NEXT_PUBLIC_ for values that are safe to expose to users. These are embedded in the client JavaScript bundle.


API Routes / Route Handlers

App Router (Route Handlers)

// app/api/data/route.ts
import { KeyEnv } from '@keyenv/node';
import { NextResponse } from 'next/server';

// Cache the client instance
let client: KeyEnv | null = null;

function getClient() {
  if (!client) {
    client = new KeyEnv({
      token: process.env.KEYENV_TOKEN!,
    });
  }
  return client;
}

export async function GET() {
  const keyenv = getClient();
  const secrets = await keyenv.exportSecretsAsObject(
    process.env.KEYENV_PROJECT_ID!,
    'production'
  );

  // Use secrets to call external APIs
  const response = await fetch('https://api.stripe.com/v1/charges', {
    headers: {
      Authorization: `Bearer ${secrets.STRIPE_SECRET_KEY}`,
    },
  });

  const data = await response.json();
  return NextResponse.json(data);
}

Pages Router (API Routes)

// pages/api/data.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { KeyEnv } from '@keyenv/node';

let secretsLoaded = false;

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  // Load secrets once
  if (!secretsLoaded) {
    const client = new KeyEnv({
      token: process.env.KEYENV_TOKEN!,
    });
    await client.loadEnv(process.env.KEYENV_PROJECT_ID!, 'production');
    secretsLoaded = true;
  }

  // Now use process.env
  const apiKey = process.env.API_KEY;

  res.status(200).json({ status: 'ok' });
}

Server Actions

Server Actions are secure server-side functions. Secrets are safe to use:

// app/actions.ts
'use server';

import { KeyEnv } from '@keyenv/node';

export async function submitPayment(formData: FormData) {
  const client = new KeyEnv({
    token: process.env.KEYENV_TOKEN!,
  });

  const secrets = await client.exportSecretsAsObject(
    process.env.KEYENV_PROJECT_ID!,
    'production'
  );

  // Safe: Server Action runs only on the server
  const response = await fetch('https://api.stripe.com/v1/charges', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${secrets.STRIPE_SECRET_KEY}`,
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: new URLSearchParams({
      amount: formData.get('amount') as string,
      currency: 'usd',
    }),
  });

  return response.json();
}

Middleware

Use KeyEnv in Next.js middleware for authentication or request processing:

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { KeyEnv } from '@keyenv/node';

// Cache secrets at module level
let cachedSecrets: Record<string, string> | null = null;

async function getSecrets() {
  if (cachedSecrets) return cachedSecrets;

  const client = new KeyEnv({
    token: process.env.KEYENV_TOKEN!,
  });

  cachedSecrets = await client.exportSecretsAsObject(
    process.env.KEYENV_PROJECT_ID!,
    'production'
  );

  return cachedSecrets;
}

export async function middleware(request: NextRequest) {
  const secrets = await getSecrets();

  // Example: Validate API key in header
  const apiKey = request.headers.get('x-api-key');
  if (apiKey !== secrets.VALID_API_KEY) {
    return NextResponse.json(
      { error: 'Unauthorized' },
      { status: 401 }
    );
  }

  return NextResponse.next();
}

export const config = {
  matcher: '/api/:path*',
};

Middleware runs on the Edge Runtime. The KeyEnv SDK is compatible with Edge Runtime environments.


Environment-Aware Configuration

Automatically select the right environment based on deployment context:

// lib/keyenv.ts
import { KeyEnv } from '@keyenv/node';

export function getEnvironment(): string {
  // Vercel provides VERCEL_ENV
  if (process.env.VERCEL_ENV) {
    switch (process.env.VERCEL_ENV) {
      case 'production':
        return 'production';
      case 'preview':
        return 'staging';
      default:
        return 'development';
    }
  }

  // Fallback to NODE_ENV
  return process.env.NODE_ENV === 'production' ? 'production' : 'development';
}

let client: KeyEnv | null = null;
let loadedEnvironment: string | null = null;

export async function getSecrets(): Promise<Record<string, string>> {
  const environment = getEnvironment();

  // Reload if environment changed (shouldn't happen, but safety first)
  if (client && loadedEnvironment === environment) {
    return client.exportSecretsAsObject(
      process.env.KEYENV_PROJECT_ID!,
      environment
    );
  }

  client = new KeyEnv({
    token: process.env.KEYENV_TOKEN!,
  });

  loadedEnvironment = environment;

  return client.exportSecretsAsObject(
    process.env.KEYENV_PROJECT_ID!,
    environment
  );
}

Usage:

// app/api/config/route.ts
import { getSecrets, getEnvironment } from '@/lib/keyenv';

export async function GET() {
  const secrets = await getSecrets();

  return Response.json({
    environment: getEnvironment(),
    // Only return non-sensitive config
    apiUrl: secrets.API_URL,
  });
}

Vercel Deployment

KeyEnv works seamlessly with Vercel. See our dedicated Vercel Integration Guide for complete setup instructions.

Quick Setup

  1. Create a Service Token in KeyEnv dashboard

  2. Add to Vercel environment variables:

    • KEYENV_TOKEN: Your service token
    • KEYENV_PROJECT_ID: Your KeyEnv project ID
  3. Environment Mapping:

Vercel EnvironmentKeyEnv Environment
Productionproduction
Previewstaging
Developmentdevelopment

For production apps, use KeyEnv's native Vercel sync:

  1. Go to Settings > Integrations in KeyEnv
  2. Connect your Vercel account
  3. Map environments
  4. Secrets sync automatically

This ensures secrets are available at both build time and runtime without any code changes.


Caching Best Practices

Singleton Pattern

Cache the KeyEnv client to avoid re-initialization:

// lib/keyenv.ts
import { KeyEnv } from '@keyenv/node';

let client: KeyEnv | null = null;
let secretsCache: Record<string, string> | null = null;

export async function getSecrets(): Promise<Record<string, string>> {
  if (secretsCache) return secretsCache;

  if (!client) {
    client = new KeyEnv({
      token: process.env.KEYENV_TOKEN!,
    });
  }

  secretsCache = await client.exportSecretsAsObject(
    process.env.KEYENV_PROJECT_ID!,
    process.env.NODE_ENV === 'production' ? 'production' : 'development'
  );

  return secretsCache;
}

// For use in Server Components
export async function getSecret(key: string): Promise<string | undefined> {
  const secrets = await getSecrets();
  return secrets[key];
}

With React Cache

For App Router, use React's cache function:

// lib/keyenv.ts
import { cache } from 'react';
import { KeyEnv } from '@keyenv/node';

export const getSecrets = cache(async () => {
  const client = new KeyEnv({
    token: process.env.KEYENV_TOKEN!,
  });

  return client.exportSecretsAsObject(
    process.env.KEYENV_PROJECT_ID!,
    process.env.NODE_ENV === 'production' ? 'production' : 'development'
  );
});

This deduplicates requests within a single render pass.


Complete Example

Project Structure

my-nextjs-app/
├── app/
│   ├── api/
│   │   └── data/
│   │       └── route.ts
│   ├── dashboard/
│   │   └── page.tsx
│   ├── actions.ts
│   └── layout.tsx
├── lib/
│   └── keyenv.ts
├── middleware.ts
├── next.config.js
└── package.json

lib/keyenv.ts

import { cache } from 'react';
import { KeyEnv } from '@keyenv/node';

function getEnvironment(): string {
  if (process.env.VERCEL_ENV === 'production') return 'production';
  if (process.env.VERCEL_ENV === 'preview') return 'staging';
  return 'development';
}

export const getSecrets = cache(async () => {
  const client = new KeyEnv({
    token: process.env.KEYENV_TOKEN!,
  });

  return client.exportSecretsAsObject(
    process.env.KEYENV_PROJECT_ID!,
    getEnvironment()
  );
});

export async function getSecret(key: string): Promise<string | undefined> {
  const secrets = await getSecrets();
  return secrets[key];
}

app/dashboard/page.tsx

import { getSecret } from '@/lib/keyenv';

export default async function DashboardPage() {
  const apiKey = await getSecret('EXTERNAL_API_KEY');

  const data = await fetch('https://api.example.com/data', {
    headers: { Authorization: `Bearer ${apiKey}` },
  });

  return (
    <main>
      <h1>Dashboard</h1>
      {/* Render data */}
    </main>
  );
}

app/api/data/route.ts

import { getSecrets } from '@/lib/keyenv';
import { NextResponse } from 'next/server';

export async function GET() {
  const secrets = await getSecrets();

  const response = await fetch('https://api.example.com/data', {
    headers: {
      Authorization: `Bearer ${secrets.API_KEY}`,
    },
  });

  return NextResponse.json(await response.json());
}

Environment Variables

Set these in your deployment environment (Vercel, etc.):

KEYENV_TOKEN=env_your_service_token
KEYENV_PROJECT_ID=proj_your_project_id

TypeScript Support

The KeyEnv SDK includes full TypeScript support:

import { KeyEnv } from '@keyenv/node';
import type { Secret, SecretWithValue } from '@keyenv/node';

For type-safe environment variables, create a declaration file:

// env.d.ts
declare namespace NodeJS {
  interface ProcessEnv {
    KEYENV_TOKEN: string;
    KEYENV_PROJECT_ID: string;
    DATABASE_URL: string;
    API_KEY: string;
    // Add your other secrets here
  }
}

Troubleshooting

"KEYENV_TOKEN is not defined"

Ensure the environment variable is set in your deployment platform (Vercel, etc.) and is available at runtime.

Secrets not available in Client Components

This is by design. Client Components run in the browser where secrets should never be exposed. Use Server Components, API Routes, or Server Actions instead.

"Error: Dynamic server usage"

If you see this error, it means you're trying to use secrets in a static context. Either:

  1. Make the page dynamic with export const dynamic = 'force-dynamic'
  2. Load secrets at build time via next.config.js

Secrets not updating after change

If using caching, you may need to redeploy. For Vercel, enable auto-redeploy in KeyEnv's native sync settings.

Edge Runtime compatibility

The KeyEnv SDK works with Edge Runtime. If you encounter issues, ensure you're using the latest version of the SDK.


Security Best Practices

  1. Never expose secrets to the client - Use Server Components, API Routes, or Server Actions
  2. Use environment-specific tokens - Create separate tokens for production and staging
  3. Scope tokens narrowly - Only grant access to required environments
  4. Use NEXT_PUBLIC_ intentionally - Only for truly public values
  5. Cache secrets - Reduce API calls and improve performance
  6. Review token permissions - Regularly audit service token access

On this page