Build and deploy a SaaS: the launch checklist I actually use
Every build-a-SaaS tutorial ends at the deploy command. The work that decides whether the first month goes well starts there: deploys you can roll back, failed jobs that announce themselves, backups you have actually restored, rate limits, an error budget and a plan for the first week on call.
The deploy goes green on a Tuesday. The landing page is live, the first customers sign up, and for about two days everything is fine. On Thursday a customer asks why their invoice never arrived. The payment went through, but the job that generates the PDF has been failing since Tuesday night because of a missing font on the production server, and it failed without telling anyone. On Saturday a script tries ten thousand passwords against the login form. On the following Wednesday someone runs a migration that locks the largest table for four minutes during the busiest hour. None of this was in the tutorial, because the tutorial ended at the deploy command.
Search for how to build and deploy a SaaS and most of what comes back is a speed run: a SaaS in thirty minutes, a weekend, a single video. They are good at what they show, and what they show stops at the moment the product meets real users. This is the checklist I work through between that moment and the end of the first month. It is written from Laravel projects, but almost every item applies to any stack.
1. Deploys you can repeat and undo
If one person knows how to release, from their laptop, you do not have a deploy process. You have a person. Before launch, a deploy should be a pipeline anyone on the team can trigger: tests, build, migrate, switch traffic, and a rollback that is one step and has been used at least once.
Migrations are where rollbacks quietly fail. Rolling back code is easy. Rolling back a dropped column is not. Write migrations so the old code and the new code can both run against the same schema: add the new column, ship code that writes to both, backfill, switch reads, and only remove the old column in a later release. It is slower by a release or two, and it means a bad deploy is a five-minute rollback instead of a restore.
2. Failed jobs that announce themselves
Queues are what make a SaaS feel fast, and they are where most silent failures live. Every job that talks to an external service (email, PDF generation, payment providers, accounting sync, webhooks out) needs four things before launch: a retry count, a backoff, a timeout, and a failure handler that leaves the record in a state someone can see and fix.
class SyncInvoiceToAccounting implements ShouldQueue
{
use Queueable;
public int $tries = 5;
public array $backoff = [10, 60, 300, 900]; // seconds between attempts
public int $timeout = 30;
public function __construct(public Invoice $invoice) {}
public function handle(AccountingClient $accounting): void
{
// The idempotency key makes a retry safe if the first attempt
// succeeded but the response never came back.
$accounting->upsertInvoice($this->invoice, key: 'invoice-'.$this->invoice->id);
}
public function failed(Throwable $e): void
{
$this->invoice->update(['accounting_sync' => 'failed']);
report($e); // reaches error tracking, and from there a person
}
}Then alert on the queue itself: on the number of failed jobs, and on how long the oldest job has been waiting. A queue whose workers have died looks exactly like a quiet afternoon until someone checks. Run the workers under a supervisor that restarts them, and restart them on every deploy so they pick up new code. The Laravel app that survives year two goes further into why slow integrations belong on their own queue.
3. Scheduled tasks you would notice stopping
Nightly billing reconciliation, trial expiry, report emails and data cleanup all run on a schedule, and a scheduler that stops running produces no errors at all. Nothing fails. Things just stop happening. The fix is a heartbeat: each important task pings a monitoring service when it finishes, and the service alerts when the ping does not arrive.
// routes/console.php
Schedule::command('billing:reconcile')
->dailyAt('02:00')
->onOneServer() // safe once you run more than one app server
->withoutOverlapping()
->pingOnSuccess(config('services.heartbeats.billing_reconcile'));4. Backups you have actually restored
Almost every hosting provider takes backups. Far fewer teams have ever restored one. The first restore is always the one that surprises you: the backup excludes a schema, the file storage is not included, the dump takes three hours to load, or the credentials to reach it are on a laptop that is no longer with the company. Find that out before launch, on a quiet afternoon, not during an incident.
Take a fresh backup the normal way
Not a special one for the drill
Restore it into a new, empty database
Time it from start to usable
Compare row counts and run the smoke tests
Point a staging copy of the app at it
↳ Mismatch: fix the backup, then repeat
Check uploaded files restore too
Object storage is often a separate system
Write down the steps and the time taken
That time is your real recovery time
Repeat it every quarter, and after any change to hosting or storage.
5. Rate limits on the endpoints that get abused first
A public launch is also an announcement to everyone running credential-stuffing scripts and signup bots. Sign-in, sign-up, password reset, any endpoint that sends an email or a text, and the public API need limits from the first day. Limit by account and by IP address together, so one attacker cannot lock a real customer out by guessing their email.
// AppServiceProvider::boot()
RateLimiter::for('login', fn (Request $request) => [
Limit::perMinute(5)->by(strtolower($request->input('email')).'|'.$request->ip()),
Limit::perMinute(30)->by($request->ip()),
]);
RateLimiter::for('api', fn (Request $request) => $request->user()
? Limit::perMinute(120)->by('tenant:'.$request->user()->tenant_id)
: Limit::perMinute(20)->by($request->ip()));6. Webhooks that cannot be faked or double-counted
Payment providers tell you about renewals, failed charges and cancellations through webhooks, and your billing state is only as correct as your handling of them. Verify the signature on every incoming webhook. Store the event id and ignore one you have already processed, because providers deliver the same event more than once. Return a success response quickly and do the work on a queue. And reconcile against the provider's own records on a schedule, because sooner or later a webhook will be missed, and a nightly comparison is how you find out before the customer does.
7. Production configuration, checked line by line
- Debug mode off. A stack trace on a public error page shows your environment to anyone who triggers one.
- Secrets out of the repository and out of the build logs, with production keys different from staging.
- HTTPS everywhere, secure and http-only session cookies, and the canonical domain redirecting the other one.
- Email authentication records (SPF, DKIM and DMARC) on the sending domain, or the password reset email lands in spam on launch day.
- A health check endpoint an uptime monitor calls every minute. Laravel ships one at
/up. - Error tracking connected and tested. Throw a test exception in production and confirm a person receives the alert.
8. An error budget, chosen before you need it
Decide how much downtime is acceptable before you have any, because that number decides how much infrastructure you need. Most early SaaS products do not need multiple regions or a cluster. They need a clear target and the monitoring to know whether they are meeting it.
- 99% a monthFine for an internal tool
- 7.2 h
- 99.5% a monthA reasonable first target
- 3.6 h
- 99.9% a monthNeeds zero-downtime deploys and fast rollback
- 43 min
The rule that makes the number useful: while there is budget left, ship normally. Once a month's budget is spent, slow down feature releases and spend the time on whatever caused the downtime. It turns reliability from an argument into a measurement.
9. The first week on call
The first week after launch is when the product meets data and behaviour you did not test with. Plan for it as a week, not a day:
| When | What to look at |
|---|---|
| Every morning | Failed jobs, new errors by count, slowest endpoints, signups that never finished onboarding |
| Every evening | Queue depth, scheduler heartbeats, payment webhooks received against charges made |
| Day 3 | The slowest database queries against real data, with the indexes they need |
| Day 5 | Support messages grouped by cause. Each group is either a bug, missing copy or a missing feature |
| Day 7 | Restore last night's backup again, now with real customer data in it |
Decide in advance who receives alerts out of hours, how they reach someone else, and what they are allowed to roll back without asking. A small team does not need a rota, but it does need everyone to know who is watching.
Two parts of the product get their own launch lists, because they are what customers see first: the landing page that has to survive the traffic it asks for, and the dashboard, before you let customers see it.
What can safely wait
- Autoscaling and Kubernetes. One well-sized server and a managed database carry most products well past their first thousand customers.
- Microservices. A modular monolith is easier to deploy, monitor and roll back, and it can be split later along lines you have learned.
- Multi-region failover. Expensive, complex and rarely needed below 99.9%.
- Complete test coverage. Feature tests over sign-up, billing and the core workflow catch more launch-week bugs than broad unit coverage.
The deploy is the easy part. A launch is ready when you would find out about a failure before your customers do, and could recover from it before they gave up waiting.
If you have a SaaS close to launch, or one that is live and has already had its first silent failure, I build and review exactly this layer: deploys and rollback, queues and failed jobs, backups with a tested restore, rate limits, webhooks and monitoring, handed over documented. Scoped and priced up front. Get a fixed-scope build quote.
Questions people ask
How do I deploy a SaaS application?
Deploy from a repeatable pipeline rather than a laptop, run database migrations that are backward-compatible with the code still running, keep queue workers and the scheduler supervised, and make sure you can roll back in one step. Before launch, also confirm backups restore, failed jobs alert someone, and error tracking and uptime checks are reporting to a person.
What should be on a SaaS launch checklist?
Repeatable deploys with rollback, backward-compatible migrations, failed-job handling and alerts, monitored scheduled tasks, a tested backup restore, rate limits on authentication and the API, verified and idempotent webhooks, production configuration with debug off and secrets out of the repository, error tracking, an uptime check, email authentication records, and a plan for the first week on call.
How do I build a SaaS step by step?
Decide the tenancy model and data model first, then build authentication, roles and billing state, then the core feature, then the operational layer: queues, scheduled tasks, monitoring, backups and deploys. Most tutorials stop after the core feature. The operational layer is what decides whether the product holds up with paying customers.
What is an error budget?
An error budget is the amount of downtime or failure your availability target allows. A 99.9% monthly target allows about 43 minutes of downtime a month; 99.5% allows about 3.6 hours. While budget remains you ship normally; once it is spent you slow releases and fix reliability first.
Building something like this?
I design and ship these systems for clients: retrieval over private data, agents that complete real tasks, and the Laravel platforms underneath them.