How to Load Environment Variables in Standalone Next.js Scripts

Say goodbye to missing env vars in your scripts—learn how to load them properly outside the Next.js runtime.

·Matija Žiberna·
How to Load Environment Variables in Standalone Next.js Scripts

If you're reading this, you've probably hit the same frustrating wall I did when trying to run database seeding scripts, sync jobs, or other standalone utilities in your Next.js project. You know that moment when your script crashes with "PAYLOAD_SECRET is not set" even though you can clearly see it in your .env.local file? Yeah, I've been there too.

In this guide, you'll learn exactly why this happens and the simple trick to fix it. We'll walk through a real world Facebook sync script example and show you multiple approaches to solve this common problem.

The Challenge I Faced

I was building a Next.js application with Payload CMS, and needed to run a Facebook sync script to pull in data from the Facebook API. My script looks something like this:

// src/scripts/facebook-sync.ts
import { getPayload } from 'payload';
import config from '@payload-config';

async function main() {
  const payload = await getPayload({ config });
  // ... rest of your sync logic
}

It can be run with npm script:

pnpm run facebook-sync

And boom! You get hit with:

Error: PAYLOAD_SECRET is not set

But wait... your .env.local file clearly has:

PAYLOAD_SECRET=your_secret_here
DATABASE_URL=postgresql://...
FACEBOOK_API_KEY=your_api_key

What gives?

Understanding the Problem

Next.js automatically loads environment variables for your web application, but standalone scripts that run outside of the Next.js runtime don't get this magic treatment. When you run a script with tsx, ts-node, or even plain Node.js, it doesn't know about your .env files unless you explicitly tell it to load them.

Next.js app works fine because the framework handles this behind the scenes, but your scripts are running in a different context entirely.

Example: Facebook Sync Script

Let's walk through fixing the Facebook sync script step by step. I'll show you three different approaches to solve this problem.

Step 1: Install Required Dependencies

First, make sure you have the necessary packages:

pnpm add dotenv cross-env tsx

This is the simplest and most reliable approach. Update your script to load environment variables at the very top:

#!/usr/bin/env tsx

// This MUST be the first import - it loads your .env files
import 'dotenv/config';

import { getPayload } from 'payload';
import config from '@payload-config';
import { runFacebookSync } from '@/lib/payload/seed';

async function main() {
  console.log('🚀 Starting Facebook sync...');
  
  // Debug: Verify env vars are loaded
  console.log('✅ Environment variables loaded:', {
    hasPayloadSecret: !!process.env.PAYLOAD_SECRET,
    hasDbUrl: !!process.env.DATABASE_URL,
    hasFacebookKey: !!process.env.FACEBOOK_API_KEY
  });

  try {
    const payload = await getPayload({ config });
    await runFacebookSync(payload);
    console.log('✅ Sync completed!');
  } catch (error) {
    console.error('❌ Sync failed:', error);
    process.exit(1);
  }
}

main().catch(console.error);

Your package.json script stays the same:

{
  "scripts": {
    "facebook-sync": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/facebook-sync.ts"
  }
}

Step 3: Method 2 - Using Next.js Built-in Env Loading

If you prefer to use Next.js's own environment loading system:

#!/usr/bin/env tsx

import { loadEnvConfig } from '@next/env';

// Load environment variables for the current project directory
loadEnvConfig(process.cwd());

import { getPayload } from 'payload';
import config from '@payload-config';
import { runFacebookSync } from '@/lib/payload/seed';

// Rest of your script remains the same...

Step 4: Method 3 - Loading via NPM Script

Alternatively, you can modify your package.json script to preload the environment:

{
  "scripts": {
    "facebook-sync": "cross-env NODE_OPTIONS=--no-deprecation tsx -r dotenv/config src/scripts/facebook-sync.ts"
  }
}

With this approach, you don't need to import anything in your script file.

Step 5: Test Your Solution

Run your script:

pnpm run facebook-sync

You should now see something like:

🚀 Starting Facebook sync...
✅ Environment variables loaded: {
  hasPayloadSecret: true,
  hasDbUrl: true,
  hasFacebookKey: true
}
📡 Initializing Payload...
🔄 Running Facebook sync...
✅ Sync completed!

Pro Tips for Environment Variable Management

  1. Order matters: Always import 'dotenv/config' or call loadEnvConfig() before importing any modules that might use environment variables.

  2. Multiple env files: dotenv automatically loads .env, .env.local, and environment specific files like .env.development in the correct priority order.

  3. Debugging: Add environment variable checks at the start of your scripts to catch missing variables early:

const requiredEnvVars = ['PAYLOAD_SECRET', 'DATABASE_URL', 'FACEBOOK_API_KEY'];
const missingVars = requiredEnvVars.filter(varName => !process.env[varName]);

if (missingVars.length > 0) {
  console.error('❌ Missing required environment variables:', missingVars);
  process.exit(1);
}
  1. TypeScript types: Consider creating type definitions for your environment variables:
declare global {
  namespace NodeJS {
    interface ProcessEnv {
      PAYLOAD_SECRET: string;
      DATABASE_URL: string;
      FACEBOOK_API_KEY: string;
    }
  }
}

Conclusion

Environment variable loading in standalone scripts is one of those "gotcha" moments that can trip up even experienced developers. The key insight is understanding that Next.js's automatic environment loading only works within the Next.js runtime, not in standalone scripts.

0

Comments

Enjoyed this article?
Subscribe to my newsletter for more insights and tutorials.
Matija Žiberna
Matija Žiberna
Full-stack developer, co-founder

I'm Matija Žiberna, a self-taught full-stack developer and co-founder passionate about building products, writing clean code, and figuring out how to turn ideas into businesses. I write about web development with Next.js, lessons from entrepreneurship, and the journey of learning by doing. My goal is to provide value through code—whether it's through tools, content, or real-world software.

You might be interested in

    How to Load Environment Variables in Standalone Next.js Scripts | Build with Matija