How to design secure API keys, store them safely in your applications, handle rotation without downtime, and detect leaked keys before they cause damage.
API keys are everywhere — OpenAI, Stripe, GitHub, Google Maps, AWS. They're simple to use but easy to misuse. A leaked API key can result in thousands in unauthorized charges, data breaches, or account takeovers. This guide covers the full lifecycle: generation, storage, usage, and rotation.
A well-designed API key has three properties:
An API key should have at least 128 bits of entropy, ideally 256 bits:
const crypto = require('crypto');
// 32 random bytes = 256 bits of entropy
const rawKey = crypto.randomBytes(32);
// Base62 encoding (URL-safe, no confusing chars)
const BASE62 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
// Or simpler: hex encoding (48 chars, still 192 bits)
const hexKey = rawKey.toString('hex'); // e.g., 'a3f2b1...'
Prefixes like sk_live_, pk_test_, or heo_ make keys identifiable in logs and prevent accidental cross-environment usage:
function generateApiKey(prefix = 'heo') {
const secret = crypto.randomBytes(32).toString('hex');
return `${prefix}_${secret}`;
}
// e.g., 'heo_a3f2b1c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1'
GitHub uses ghp_ (personal access tokens), ghs_ (server tokens), and gha_ (Actions tokens). Stripe uses sk_live_ and sk_test_. This approach also enables GitHub's secret scanning to automatically detect leaked keys.
Like passwords, API keys should be hashed before storage. This way, even if your database is breached, attackers get hashes, not working keys:
const crypto = require('crypto');
// Hash the key for storage
function hashApiKey(key) {
return crypto.createHash('sha256').update(key).digest('hex');
}
// On creation: store the hash, return the key once to the user
async function createApiKey(userId) {
const key = generateApiKey();
const keyHash = hashApiKey(key);
await db.apiKeys.insert({
user_id: userId,
key_hash: keyHash,
key_preview: key.slice(0, 8) + '...' + key.slice(-4), // for UI display
created_at: new Date(),
});
return key; // Show this ONCE — never stored
}
// On verification: hash the incoming key and compare
async function verifyApiKey(key) {
const keyHash = hashApiKey(key);
return db.apiKeys.findOne({ key_hash: keyHash, revoked: false });
}
SHA-256 is fine here (unlike passwords) because API keys already have high entropy — rainbow tables aren't viable against 256-bit random values.
// ❌ Never do this
const openai = new OpenAI({ apiKey: 'sk-abc123...' });
// ✅ Always use environment variables
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
# .env.local (never commit this file)
OPENAI_API_KEY=sk-...
STRIPE_SECRET_KEY=sk_test_...
Add .env.local to .gitignore. Commit a .env.example with placeholder values.
Use your platform's secret management — never plain environment variables in a docker-compose.yml or CI config file:
If you suspect a key is leaked:
git log -S 'sk_' to find historical commits containing keysIf you use prefixed API keys (like heo_), you can register a pattern with GitHub's Secret Scanning Partner Program. GitHub will automatically alert you when a key with your pattern appears in a public repository.
Don't make keys omnipotent. Scope them to minimum required permissions:
-- API keys table with permission scopes
CREATE TABLE api_keys (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id),
key_hash TEXT UNIQUE NOT NULL,
key_preview TEXT NOT NULL,
scopes TEXT[] DEFAULT '{}', -- e.g., '{read:data,write:data}'
rate_limit INT DEFAULT 1000, -- requests per hour
expires_at TIMESTAMPTZ,
revoked_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
);
Use HeoLab's API Key Generator to create cryptographically secure API keys with custom prefixes, lengths, and character sets — perfect for testing your key generation logic or generating development keys instantly.