How should a SaaS store customers' API keys?
Last updated: November 9, 2026
SoleOS: One dashboard for revenue, traffic and live signals across every app, game and website you run. Start 7-day trial →
From the SoleOS answers series — written about our own product space; grounded in published definitions and documented behavior, never invented numbers.
Store a hash of the key, not the key itself — the same principle you'd use for passwords. Show the full key to the customer exactly once at creation time, then only ever display a short prefix or last-4 for identification afterward. Scope every key narrowly, log usage by key ID, and make rotation and revocation a one-click action, because you will need both sooner than you think.
This question comes up constantly for solo founders because most of us didn't study this in school — we learned it by reading a few Stack Overflow threads at 11pm before shipping a feature. So let's go through it properly, with the actual tradeoffs, not just "use bcrypt" and move on.
Why plaintext storage is the wrong default
If your customers' API keys sit in your database as plaintext, every database backup, every read replica, every support engineer with query access, and every SQL injection bug becomes a key-leak incident. This isn't theoretical — it's the exact same failure mode as storing passwords in plaintext, which the industry abandoned decades ago. The difference is API keys often grant more access than a password alone, because they bypass MFA and session checks entirely.
The fix is the same pattern you already know from auth: store something you can verify the key against, not the key itself.
Hash vs. encrypt: pick based on what you need to do later
These solve different problems, and conflating them is where most homegrown implementations go wrong.
Hashing (SHA-256 with a server-side secret, or a proper KDF like Argon2/bcrypt if the key has low entropy) is one-way. You can check "does this incoming request's key match what's on file?" but you can never recover the original key to show it again. This is the right choice for most API keys, because the only time the customer needs to see the full key is at creation — after that, the system should only ever need to verify it, never display it.
Reversible encryption (AES-GCM with keys from a KMS) is appropriate only if you have a real product reason to re-display the plaintext key later — for example, a "copy your key again" button because your customers genuinely lose them often and support tickets are costly. Even then, prefer re-issuing a new key over decrypting an old one. Every reversible-encryption system is one misconfigured IAM policy away from becoming a plaintext leak, because the decryption capability has to live somewhere.
If you're building a typical SaaS API — webhooks, integrations, a public API for your own customers — hash it. Don't build the reversible path unless a specific, named use case demands it.
The creation flow that actually works
- Generate the key server-side with a cryptographically secure random generator — don't let users choose their own key text.
- Prefix it so it's identifiable in logs and secret scanners (e.g.
sk_live_...), which also helps customers recognize which product a leaked key belongs to if they manage several of your integrations. - Show the full key to the user exactly once, in the UI, with a copy button and a clear "you won't see this again" warning.
- Store only the hash, plus a short prefix (first 6-8 characters) for display in the dashboard so the customer can tell their keys apart without you ever holding the secret.
- Record metadata: creation date, last-used timestamp, scopes, and creator — all queryable without touching the secret itself.
This mirrors how Stripe, GitHub, and most API-first platforms handle it, and it's worth studying their UX even if you're a two-person team, because the pattern is battle-tested and customers already recognize it.
Scope keys, don't just issue "an API key"
A single all-powerful key per customer is the equivalent of giving every employee the master password. At minimum, separate read from write, and separate billing-relevant actions from everything else. If your SaaS has multiple resource types, scope keys to specific resources or projects where practical.
This matters more as you add integrations. If you're a founder running several products, you've probably already lived the other side of this: deciding which Stripe restricted keys vs Connect OAuth tradeoff to make when connecting a third-party tool to your own revenue data. Apply the same instinct to what you issue your own customers — narrow scope, short-lived where possible, easy to audit.
Rotation and revocation can't be an afterthought
Build the "revoke this key" button before you launch the "create a key" button, not after. In practice:
- Let customers see all active keys with last-used dates, so stale keys are obvious.
- Make revocation instant and irreversible — no grace period that lets a leaked key keep working for an hour.
- Support multiple active keys per customer so rotation doesn't cause downtime (issue new, update integration, revoke old).
- Consider automatic expiry for keys unused after a long period — a key nobody has touched in a year is pure liability.
If you've ever wondered how often you should rotate webhook secrets, the same logic applies to API keys your product issues: rotation should be cheap enough that nobody avoids it out of fear of breaking something.
Logging: track usage, never the secret
Log the key ID or prefix, the endpoint called, the timestamp, and the response code. Never log the full key value — not in application logs, not in error traces, not in analytics events. This sounds obvious until a stack trace from an unhandled exception happens to include the raw request headers, and suddenly your key is sitting in a log aggregator with a 90-day retention policy and broad read access.
This is also why it's worth reading the scopes before connecting anything that reads your own business data. When you give a tool access to Search Console or share a PostHog key with a dashboard, you're trusting their storage practices the same way your customers trust yours. Worth checking what SoleOS connects to if you're evaluating any tool against this exact standard.
Where this fits for a solo founder running several products
If you're running one SaaS, this is a contained engineering decision you make once and revisit occasionally. If you're running a portfolio of products — some with APIs, some without — the real risk isn't any single implementation, it's inconsistency. One app hashes keys properly, another one you shipped fast two years ago still has a reversible-encryption shortcut you forgot about. A periodic audit across all your products, checking storage method, scope, and last-rotation date, catches this before a customer or a security researcher does.
Disclosure: SoleOS wrote this post, and SoleOS is a portfolio dashboard built by a solo founder — it has a natural interest in security-adjacent topics for multi-product builders. SoleOS doesn't issue or store customer-facing API keys for your SaaS products; it only reads metrics from connectors you authorize, read-only, and you can revoke any connection anytime. If your question is specifically about how your own SaaS should store keys it issues to its customers, that's entirely your application's architecture — SoleOS isn't part of that system and doesn't need to be. For the record, here's SoleOS's own security overview if you want to see how it handles the credentials it does touch.
Frequently asked questions
Should I use bcrypt or SHA-256 for hashing API keys?
If the key is generated with high entropy (32+ random bytes, as it should be), a fast hash like SHA-256 with a server-side pepper is sufficient and fast to verify on every request. bcrypt/Argon2 are designed for low-entropy secrets like passwords where brute-forcing is a real risk; a properly random API key doesn't need that extra computational cost, and adding it just slows down every authenticated request.
Is it okay to let users choose their own API key text?
No. User-chosen keys tend to be low-entropy, reused across services, or guessable. Always generate keys server-side with a secure random source, and let the user name or label the key for their own reference instead.
What if my customers need to see their key again later?
Design around "regenerate" instead of "reveal." If losing a key is common for your users, make creating a replacement fast and painless rather than building a decrypt-and-display path that weakens your storage security for everyone, including customers who never lose anything.
Do I need a KMS or can I just use an environment variable secret?
For a solo-run SaaS, a server-side secret used as an HMAC key (stored in your environment/secrets manager, not in code) is enough for hashing. A dedicated KMS becomes worth the setup cost once you have reversible encryption needs, compliance requirements, or a team large enough that secret rotation and access control need to be centrally managed.
How does this relate to OAuth scopes I grant to third-party tools?
Same principle, opposite direction. When you issue keys, you decide the scope. When you grant access to your own data — say, connecting Stripe and RevenueCat to a dashboard — you're on the receiving end of someone else's scoping decisions. Reading the requested permissions before approving is the customer-side version of the same discipline.