Security by design is not a framework you adopt. It is a short list of decisions you make before the first release, while changing your mind still costs nothing. The asymmetry is the whole point: deciding not to collect a field costs zero before launch and a migration plus a forced client update afterwards. This article is that list, in the order a delivery team actually hits it.
The core of the question
Most teams treat security as a phase: build the product, harden it, then write the security requirements down for the client questionnaire. That order works for things that sit on top of what you already built — dependency scanning, penetration testing, rate limits. It fails for anything touching your data model, your permission model, or your defaults, because those are load-bearing. Once real users depend on them you are not making a decision anymore, you are running a migration.
So the practical definition: security by design means putting the structurally irreversible decisions at the start of the project and leaving the cheap-to-reverse ones for later. No certification, no dedicated security engineer, no new process document — just knowing which is which. Most of these decisions shrink the threat surface by removing something rather than by adding a control on top of it.
Here is the split that matters:
| Decision | Cost before first release | Cost after real users |
|---|---|---|
| Not collecting a data field | Zero — one line in a schema | Migration, backfill, client update, deletion requests |
| Where tokens and secrets live | Zero — pick the place once | Rotating every secret that touched the wrong place |
| Defaults for shared data | Zero — flip a boolean | Users rely on the open default; tightening looks like a regression |
| Granularity of permissions | Low — actions, not roles | Rewriting every authorisation check |
| Separate environments and test data | Low — one more pipeline | Production data already copied where you cannot account for it |
| What goes into logs | Zero — a field list | Retroactive scrubbing, and you never learn who read them |
Everything in the left column is a conversation at kickoff. Everything in the right column is a project.
How it works: the decisions, one at a time
Decide what you will never collect
Start with the data model, not the threat model. For each field, ask what breaks if you do not have it — not “what could we do with it someday”, but what breaks in the feature you are shipping. Date of birth when an age bracket is enough. Full address when a country is enough. Precise location when a city is enough. Free-text notes that end up holding things nobody planned to store.
Data you never collect cannot leak, cannot be mishandled by a third-party SDK, and needs no retention policy — the only security control with a negative operating cost. It is also the one that becomes impossible later: once a field exists and something reads it, removing it means touching the schema, the API, the client, and every export that depends on it.
Data minimisation is where privacy by design stops being a slogan. As a slogan it means “we care about privacy” and produces nothing. As a decision it produces a shorter table.
Decide where secrets live before anyone needs one
The first API key of a project gets committed to the repository because there is nowhere else to put it yet. That is the actual mechanism — not carelessness, just absence of a destination. So create the destination in week one: secrets in the environment or a secret store, injected at deploy time, never in the repository, never in client-side code, never in a shared document.
Two rules pay for themselves. Anything a client holds is public — if the client can read it, a user can read it, so keys that authorise anything meaningful stay server-side and the client gets a short-lived token. And a secret that entered the wrong place is burned: you rotate it rather than delete the commit.
Make the default inconvenient
This is the part teams argue about, so be explicit: secure by default means the out-of-the-box state is the restrictive one, and openness is something a user chooses on purpose. New record visible only to its owner. Sharing off. Export off. Integration scopes empty. Public link generation disabled.
It costs nothing now because there are no users to annoy, and it is nearly impossible later, because by then somebody built a workflow on the open default and tightening it reads as breakage rather than a fix. The cost is worth naming: a restrictive default means more steps, and someone will file that as friction. The response is not to loosen the default but to make opening something fast and legible — one toggle, one statement of who gains access, one way to revoke it. The alternative is a user who never knew their data was shared.
Model permissions as actions, not job titles
Roles feel natural at the start and calcify into the wrong shape by month six, with “Admin” accumulating everything nobody wanted to think about. The durable version is least privilege at the level of actions: who may read this record, who may modify it, who may export a set of them, who may act on behalf of someone else. Roles then become named bundles of actions, and an odd permission for one user stops requiring a new role.
The reason to do this first is mechanical: action-level checks are enforced at one boundary you can audit, while role checks scatter, and converting scattered checks into a coherent model means touching every endpoint at once. A practical baseline for the rest of the stack — encryption in transit and at rest, server-side validation, no stack traces in user-facing errors — is covered in our notes on security in web application development; the authorisation model is the piece you cannot retrofit cheaply.
Separate environments, and separate the data too
Separate environments is the easy half and most teams do it. The half that gets skipped is separate data. A production dump in the staging database is the most common way real personal data ends up somewhere with weaker access control, older code, and more people holding credentials. Generate test data instead, or anonymise on the way out with a script that runs in the pipeline rather than on someone’s laptop — cheap in week one, awkward at month eight, once the test suite quietly depends on the shape of real records.
Decide what logs are for
Logs are how you debug and reconstruct an incident, so they need to be rich. They are also read by more people than your database and retained longer than anyone intends. Write the field list at the start: request identifiers, user identifiers, actions, outcomes, timestamps — yes. Credentials, tokens, session identifiers, full request bodies from authenticated endpoints, and the personal data you just worked to minimise — no.
Then add the thing most teams add late: an audit trail for sensitive reads and permission changes, separate from debug logs, kept for a defined period. “Who looked at this record” is a question you will eventually be asked, and only data you were already collecting can answer it.
An example from our practice
For PandaSmile, built with Dr. Olivier Setbon in Paris, we developed a digital orthodontic care application in React Native and Laravel — gamified exercises for patients plus push reminders to keep them on schedule.
The product handles patient data for users in the EU, which meant the decisions above were project decisions at the start rather than a hardening pass before launch. Concretely: we went through the data model field by field and cut what the exercise and reminder features did not need, so the application never stored it. Access was split so that the clinical side and the patient side could not reach each other’s data by default, with permissions defined per action rather than by a general practitioner role. Secrets and tokens stayed server-side, with the mobile clients holding short-lived credentials only. Logs carried identifiers and outcomes for debugging, not clinical content.
None of it was expensive — a set of choices made before there was anything to migrate. The counterfactual makes the point: dropping a patient data field after release would have meant a schema migration, a deletion path, and an app store update rolled out to every installed client, for a field nobody needed.
What to watch out for
Treating it as a kickoff ritual. A one-off session at project start produces decisions that quietly expire. The durable version is a standing item: every feature that adds a field, an endpoint, or an integration answers three questions in the same review as everything else — what data does this collect, who can reach it, what is the default. That is what makes it a secure software development lifecycle rather than a memo.
Writing security requirements nobody can check. “The system shall be secure” is not a requirement. “No endpoint returns another user’s records without an explicit grant, and there is a test for it” is. Requirements that cannot fail a test do not survive a deadline.
Confusing this with compliance. A regulation tells you what to be able to demonstrate; security by design tells you which decisions to make early so demonstrating it later is possible. Design work does not make a product certified, and claiming otherwise is a separate and worse problem.
Over-restricting where it does not matter. Not every field is sensitive and not every default needs closing. Spending the team’s patience on low-stakes restrictions burns credibility you need for the ones that count.
Inheriting a running system. The free window has closed, but the decisions still apply to everything you add from here, and a review tells you which parts of the existing surface are worth a migration. What that looks for is described in what an AI code audit really includes.
Conclusion
Security by design comes down to sequencing. Decide what you never collect, where secrets live, what the defaults are, how permissions are shaped, how environments and test data are separated, and what logs hold — all while the cost of changing your mind is a line in a schema. Everything else can be added on top later without a migration. It is the same reasoning we apply in fintech work, where the pattern is identical and the stakes are louder about it: building security into the architecture from the start rather than adding it later. If you are starting a build and want these decisions made deliberately, that is part of how we scope custom software development.


