The first week of every engagement, before we write a line of product code, we build the deployment pipeline. Not a stub. Not "we will set this up properly later." The full thing: environments, CI, secrets management, observability, and a working deploy to production on day five.
Clients sometimes push back on this. The product does not exist yet. Why are we spending time on infrastructure? The answer is that without a pipeline, what you are building is not software. It is a local prototype that nobody can use, cannot be reviewed safely, and will require a painful migration to production at exactly the moment you have the least time for it.
We have seen what happens when teams skip this step. The codebase grows for three months. The first real deploy takes two weeks. Configuration assumptions that looked fine locally turn out to be wrong in production. A secret ends up committed to git. The staging environment never quite matches production, so bugs appear that nobody saw in testing. All of this is avoidable, and none of it is complicated to avoid.
A working deploy on day five means every day after is real engineering, not prototype building. The pipeline is not overhead. It is the ground you build on.
The four environments
Every project runs four environments: local, preview, staging, and production. Each has a defined purpose. Collapsing them is where problems start.
Local is where engineers write code. It runs entirely on the developer's machine with no shared services and no shared databases. Each engineer gets their own isolated instance. Shared local databases cause data corruption, hidden dependencies, and debugging sessions where the problem turns out to be somebody else's migration that did not run cleanly.
Preview is an ephemeral environment created automatically for every pull request. It runs the full application with real dependencies against a snapshot of the staging database. It lives for the life of the PR and is torn down on merge. The purpose is to let reviewers see the change running, not just read the diff. For frontend work this is especially valuable: design review in a browser beats reading JSX.
Staging is the closest approximation of production. Same infrastructure, same configuration structure, same third-party integrations pointed at sandbox endpoints. It is where QA happens and where the client accepts work before it goes live. One rule: if something is true in production, it must be true in staging. Any divergence between the two is a bug in the pipeline, not in the application.
Production is production. Only staging-tested code reaches it. Deploys are automated. No manual SSH, no file uploads, no pushing directly. The pipeline is the only path to production, without exception.
CI from commit one
Continuous integration runs on every push to every branch. The suite includes linting, type checking, unit tests, and integration tests. It runs in under four minutes. If it takes longer than four minutes, engineers stop waiting for it and start merging blind, which defeats the point.
The four-minute constraint forces discipline. You cannot add slow tests without removing or parallelising existing ones. You cannot skip linting because "it takes too long." The constraint is the policy.
Branch protection is on from day one. No PR merges without green CI. No exceptions, including for the team lead and including for urgent hotfixes. If the hotfix is so urgent that you cannot wait four minutes, the incident response process is the wrong place to be discovering that the tests are broken.
We also run dependency vulnerability scanning in CI. Not as a blocking check from the start, but logged from the beginning so there is never a moment where the team discovers they have been shipping known vulnerabilities for six months. For a more detailed treatment of the security surface, see our piece on startup attack surfaces.
The deployment checklist
Every production deploy runs through the same checklist, encoded in the pipeline, not in someone's memory.
| Step | What it verifies | Blocks deploy if failing |
|---|---|---|
| CI suite | Tests, types, lint | Yes |
| Dependency audit | No critical CVEs in direct dependencies | Yes |
| Migration dry-run | Database migrations apply cleanly against a staging snapshot | Yes |
| Smoke tests | Critical user paths return expected responses | Yes |
| Rollback verification | Previous version is tagged and rollback script is tested | No, but logged |
The migration dry-run deserves emphasis. Database migrations are the most common cause of deploy failures in production systems. Running them against a snapshot of real data in staging catches column type mismatches, missing indexes on large tables, and constraint violations that unit tests never reveal. We have caught production-breaking migrations this way on several client projects.
Observability before features
On day one, before any product code, we configure three things: structured logging, error tracking, and uptime monitoring. Not dashboards. Not alerting rules. Just the plumbing so that when something breaks there is a trail to follow.
Structured logging means every log line is JSON with a consistent schema: timestamp, level, service, request ID, user ID where applicable, and the message. Free text logs are unsearchable at scale. A consistent schema means you can write a query in thirty seconds that would otherwise take thirty minutes of grepping.
Error tracking is not optional and is not the same as logging. Logging tells you what happened. Error tracking tells you what broke, how many times, for which users, and whether it is getting better or worse. It is configured before the first endpoint is written.
Uptime monitoring is a single HTTP check against the health endpoint, with an alert to a shared channel if it fails for more than two minutes. Crude, but the number of times this has caught a silent failure that no engineer noticed for hours is not small.
Secrets and configuration
No secret ever touches the repository. Not in a .env file, not in a comment, not in a test fixture, not in a TODO. The repository is treated as permanently public, regardless of its actual visibility.
Secrets are managed through the platform's secret store and injected at deploy time as environment variables. The application reads them from the environment. It does not know and does not care where they came from.
Configuration that varies by environment follows the same pattern. A database URL is not hardcoded. A feature flag threshold is not a constant. Everything that needs to differ between staging and production is an environment variable. This is boring and correct.
What happens at handoff
At the end of an engagement, the client receives the pipeline intact. Not a diagram of it. Not documentation describing it. The running pipeline, with credentials transferred, runbooks written, and at least two engineers on the client side who have run a deploy themselves.
This is the part most agencies skip. The code is delivered. The infrastructure is standing. But nobody on the client side knows how it works, so it gradually degrades as shortcuts accumulate and the original setup decisions are forgotten. Three years later, a new team inherits something they do not understand and cannot safely change.
We build the pipeline to be understood, not just to work. The scripts are readable. The configuration is documented at the point of decision. Every part of the system has an owner on the client side before we close the engagement.
The pipeline is not a deliverable. It is the foundation everything else is built on.