Secrets management for a SaaS means keeping API keys, database credentials, and signing secrets out of your client bundle and your Git history, storing them server-side as environment variables or in a managed secrets store, scoping each key to the least privilege it needs, and rotating keys on a schedule and immediately after any suspected exposure. The single most common cause of a SaaS breach in small teams is not a clever attacker — it is a secret committed to a public repo or shipped to the browser, where anyone can read it. Get the storage and scoping right and you remove the majority of that risk.
This is a supporting guide under the pillar SaaS Security and Compliance. If you want the full picture — auth, data isolation, audit logging, and the compliance frameworks — start there. This post zooms into one slice: where your secrets live and how you handle them.
Key takeaways
- Never ship a secret to the browser. Anything in your client-side bundle is public. Stripe secret keys, database service-role keys, and third-party API tokens belong only on the server.
- Keep secrets out of Git. Use a
.envfile that is gitignored, plus your host's environment-variable store (Vercel, Netlify, etc.). A leaked key in commit history is leaked forever — rotate it, do not just delete the commit. - Scope keys to least privilege. Use restricted or publishable keys where the provider offers them, separate keys per environment, and never reuse one master key everywhere.
- Rotate on a schedule and after exposure. Build rotation in early — code that reads from env vars rotates painlessly; code with hardcoded keys does not.
- Separate public from secret keys deliberately. Some keys (Stripe publishable, Firebase web config) are designed to be public and protected by other layers; some (service-role, secret keys) must never leave the server. Know which is which.
What counts as a secret in a SaaS app?
A secret is any value that grants access or trust if someone else gets hold of it. In a typical SaaS that includes:
- Payment provider secret keys — Stripe secret keys can create charges, issue refunds, and read customer data.
- Database admin credentials — Supabase service-role keys or Firebase Admin SDK credentials bypass row-level security and access every tenant's data.
- Third-party API tokens — email senders (Resend), AI APIs, mapping providers, analytics.
- Signing and encryption secrets — JWT signing keys, webhook signing secrets, session encryption keys.
- OAuth client secrets — for social login or integrations.
Notice the pattern: every one of these can act on behalf of your whole application. That is why they cannot live in the browser. A useful mental model — if leaking a value lets someone impersonate your backend or read data they should not, it is a secret and belongs server-side only.
Where should secrets actually live?
Local development: a .env file at the project root, added to .gitignore before your first commit. Commit a .env.example with the names of the variables and dummy values so collaborators know what to set, but never the real values.
Production: your hosting platform's environment-variable store. On Vercel, Netlify, or similar, you set these in the dashboard and the platform injects them at build or runtime. They are never written to disk in your repo and never reach the client unless you explicitly prefix them for the browser.
This last point is where frameworks bite people. In Next.js, any variable prefixed NEXT_PUBLIC_ is inlined into the client bundle — that is by design, for values that are meant to be public. A secret accidentally given that prefix is shipped to every visitor. The rule: secret keys get no public prefix, ever.
When I built Callidus, a clinic SaaS on React, TypeScript, and Firebase, the Stripe Connect Standard integration and the Firebase Admin operations all ran server-side. The browser only ever held publishable keys and the public Firebase web config — which is safe precisely because the actual data protection lives in per-tenant Firestore security rules keyed off JWT tenantId claims, not in hiding the config. That separation between "public by design" and "secret, server-only" is the whole game.
For a deeper look at how those tenant rules enforce isolation, see Firestore rules vs Postgres RLS for multi-tenant SaaS.
Why is shipping a key to the browser so dangerous?
Because the browser bundle is fully readable. Open dev tools, look at the network tab or the JavaScript source, and any string baked into the build is right there. There is no obfuscation that helps — minification is not encryption. Bots scan public sites and public Git repos continuously for key patterns, and exposed payment or cloud keys are often abused within minutes.
The damage scales with the key's scope. A leaked publishable key is mostly harmless because it can only do public-safe things. A leaked Stripe secret key can drain refunds and read your customer list. A leaked database service-role key can dump or delete every tenant's data, ignoring the security rules you carefully wrote. This is exactly why scoping matters as much as storage — a key that escapes should be able to do as little as possible.
If your data-isolation model itself is shaky, no amount of secret hygiene saves you. That foundation is covered in multi-tenant SaaS architecture.
How do you scope keys to least privilege?
Least privilege means each key can do only what that specific use needs, and nothing more. Concretely:
- Use restricted keys when the provider offers them. Stripe lets you create restricted API keys with read-only or resource-specific permissions. A key used only to read invoices should not be able to issue refunds.
- Separate keys per environment. Test keys for development and staging, live keys for production, stored in separate environment configs. A leaked test key cannot touch real money.
- Separate keys per service or function. If a background worker only sends email, give it an email API token, not your master credentials.
- Prefer public-safe keys client-side. Publishable keys, anon keys, and public web configs are built to be exposed and are protected by other layers (rules, RLS, server validation). Use those in the browser; keep the privileged variants on the server.
The payoff is containment. When something does leak — and over a long enough timeline, something will — a tightly scoped key limits the blast radius to one capability instead of your entire account.
How and when should you rotate keys?
Rotation means replacing a key with a fresh one and invalidating the old. You rotate in two situations: on a regular schedule (quarterly is a reasonable default for most small SaaS), and immediately whenever a key might be exposed — a public commit, a leaked log, a departing contractor, a compromised laptop.
The practical secret to painless rotation is to read every key from an environment variable, never hardcode one. When a key lives in one config entry, rotating it is: generate the new key at the provider, update the env var, redeploy. When keys are scattered as string literals across the codebase, rotation becomes an archaeology project and teams avoid it — which is how stale, over-privileged keys pile up.
A few rotation habits worth building in early:
- Keep an inventory. A short list of every secret, where it is used, and which provider issues it. You cannot rotate what you have forgotten.
- Support overlap where possible. Some providers let old and new keys both work briefly, so you can roll out without downtime.
- Log rotation events. Record when each key was last rotated. This also feeds compliance evidence if you ever pursue SOC 2 — see the SOC 2 Type 1 90-day checklist.
What does this look like in a real small-team build?
The constraints are the same whether you are solo or a small team — you just have fewer hands, so the discipline has to be baked into how the project is set up rather than enforced by process.
Across my own production builds the pattern is consistent. Callidus, built solo over roughly ten weeks (mid-February to late April 2026) after a failed FlutterFlow attempt that hit around 200 errors, kept all Stripe Connect Standard and Firebase Admin work on the server, with the browser holding only public-safe keys. BookBed — a Flutter and Firebase booking SaaS that runs from one codebase across six OS platforms (iOS, Android, web, macOS, Linux, Windows) and does bidirectional iCal sync, priced at nine euros a month for up to twenty units — keeps its payment and sync credentials server-side, never in the client distributed to six platforms. Pizzeria Bestek, a React and Supabase build with four-language support (EN/DE/IT/HR) and Supabase Realtime, used the publishable anon key in the browser and the service-role key only in server functions.
None of that is exotic. It is the same three rules each time: secrets server-side, scoped tight, read from env so they can rotate. The reason a solo developer can ship secure SaaS at a fraction of agency cost is partly this — getting the secrets model right from day one is cheap, while retrofitting it after a leak is expensive. If you want to think about how a solo build compares on cost and scope, that is its own topic in the pillar.
If you are handling EU customer data on top of all this, secrets hygiene is necessary but not sufficient — you also need a data-handling model. That is covered in GDPR for B2B SaaS architecture.
FAQ
Is the Firebase web config a secret I need to hide? No. The Firebase web config (API key, project ID, etc.) is designed to be public and ships in the browser. It identifies your project but grants no access on its own — your protection comes from Firebase security rules. What you must keep server-side is the Firebase Admin SDK credential, which does bypass rules.
What do I do if I already committed a secret to Git? Rotate it immediately at the provider, then remove it from the codebase. Do not assume deleting the commit is enough — once a value is in history (and possibly already scraped), the only safe fix is to invalidate the old key by rotating. Treat the old key as compromised regardless.
Are environment variables enough, or do I need a dedicated secrets manager?
For most small and early-stage SaaS, environment variables on your hosting platform (gitignored .env locally, dashboard config in production) are enough. A dedicated secrets manager adds value as you grow — centralized rotation, access auditing, fine-grained permissions — but it is not a prerequisite for a secure launch.
