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.

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
Step 2: Method 1 - Using dotenv/config (Recommended)
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
-
Order matters: Always import
'dotenv/config'
or callloadEnvConfig()
before importing any modules that might use environment variables. -
Multiple env files: dotenv automatically loads
.env
,.env.local
, and environment specific files like.env.development
in the correct priority order. -
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);
}
- 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.