- Book: Ship It — The Complete Tech Stack Guide for Startup Founders
- My project: Hermes IDE | GitHub
- Me: gabrielanhaia
The bottleneck moved. It used to be writing the code. Now it's everything else.
I've watched this play out a dozen times in the last year. Someone prompts their way to a working SaaS over a weekend. The code compiles. The features work. The UI looks better than half the things on Product Hunt. Then the app sits on localhost for weeks, sometimes months, because the person who built it has no idea how to turn it into something people can actually use and pay for.
The AI tools are great at code. They're useless at infrastructure decisions. Claude won't tell you whether to pick Hetzner or Railway. GPT won't set up your Cloudflare DNS. Copilot definitely won't figure out your VAT obligations in 27 EU countries. That's all on you.
The list nobody gives you
Here's everything that sits between "the app works on my laptop" and "people are paying me for this." It's not short.
- Hosting (where does this thing actually run?)
- Domain + DNS + HTTPS
- Database backups (not the database itself, the backups)
- Payment processing
- Tax compliance (VAT, sales tax, GST, depending on where your customers are)
- Authentication (please don't build this yourself)
- Security headers, input validation, secrets management
- GDPR / CCPA compliance
- Error tracking and monitoring
- CI/CD pipeline
I'm going to walk through the ones that trip people up the most and show you what "good enough for production" actually looks like.
Hosting: you're overthinking it
A $4/month VPS handles more traffic than most startups will ever see. I mean that literally. A Hetzner CX22 (2 vCPU, 4GB RAM, NVMe storage) serving a Node.js or Go API behind Caddy can handle thousands of concurrent connections without breaking a sweat.
The setup takes about 20 minutes:
# On a fresh Hetzner Ubuntu 24.04 VPS
sudo apt update && sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
# Install Caddy (handles HTTPS automatically, no certbot nonsense)
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update && sudo apt install caddy
Your Caddyfile is four lines:
myapp.com {
reverse_proxy localhost:3000
}
That's it. Caddy gets you HTTPS with auto-renewed certificates, HTTP/2, and a reverse proxy to your app. No nginx config files. No Let's Encrypt cron jobs. Four lines.
The mistake everyone makes: Thinking they need AWS, Kubernetes, or a $50/month PaaS. At the pre-revenue stage, all of that is burning money and time you don't have. A VPS is a Linux box you control. It runs whatever you want. It costs less than your morning coffee.
Database backups: the thing you'll wish you'd done
Setting up Postgres isn't the hard part. Everyone remembers to install the database. Almost nobody remembers to set up backups until the first time they lose data.
This script takes 5 minutes to set up and will save your startup one day:
#!/bin/bash
# /usr/local/bin/backup-db.sh
# Runs daily via cron, dumps Postgres to Cloudflare R2
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="/tmp/db_backup_${TIMESTAMP}.sql.gz"
BUCKET="my-app-backups"
# Dump and compress
pg_dump -U myapp myapp_db | gzip > "$BACKUP_FILE"
# Upload to R2 (uses rclone — configure once with `rclone config`)
rclone copy "$BACKUP_FILE" "r2:${BUCKET}/daily/"
# Keep last 30 days locally, R2 handles its own retention
find /tmp -name "db_backup_*.sql.gz" -mtime +30 -delete
echo "Backup completed: ${BACKUP_FILE}"
# Add to crontab: runs every day at 3 AM
# crontab -e
0 3 * * * /usr/local/bin/backup-db.sh >> /var/log/db-backup.log 2>&1
Cloudflare R2 gives you 10GB free with zero egress fees. That's months of daily backups for a typical startup database. You won't pay a cent until your database is big enough that you probably have revenue to cover it.
Don't wait. Set this up the same day you set up the database. I once worked on a project where "we'll do backups next week" turned into "we lost two days of user data." It takes 5 minutes. The alternative is losing everything and starting over.
Payments: the tax decision that changes everything
Stripe is the default answer. Great API, great docs, works everywhere. But picking Stripe means you've also picked a second job: tax compliance.
When you sell through Stripe directly, you are the merchant. That means:
- You collect sales tax in US states where you have nexus
- You collect VAT in every EU country where you have customers (all 27 of them)
- You file returns, handle exemptions, and hope you didn't screw up a rate
The alternative is a Merchant of Record (MoR). Companies like Lemon Squeezy or Paddle sit between you and the customer. Technically, they sell your product and pay you. They handle all tax collection, remittance, and compliance. You get a payout. That's it.
| Stripe (direct) | Merchant of Record | |
|---|---|---|
| Fee | 2.9% + $0.30 | ~5% + $0.50 |
| Tax handling | You do it (or pay for Stripe Tax) | They do it |
| Invoices | You generate them | They generate them |
| Refunds | You handle them | They handle them |
| Control | Full | Less (their checkout, their rules) |
For a solo founder selling a SaaS at $20/month, the math works out like this: Stripe takes ~$0.88 per transaction. Lemon Squeezy takes ~$1.50. That's $0.62 more per customer per month. In exchange, you don't need to think about tax in 100+ jurisdictions.
At 100 customers, that's $62/month for tax peace of mind. At 1,000 customers, it's $620/month and you should probably switch to Stripe + a tax tool. But by then you can afford to.
A founder I know got a €12,000 VAT bill from Ireland because they'd been selling to EU customers for 18 months without collecting VAT. They picked Stripe because "it's the standard" and didn't realize they were now on the hook for global tax compliance. That kind of surprise doesn't happen with a Merchant of Record.
Authentication: stop building this yourself
I've seen it too many times. "It's just a login form, how hard can it be?" Famous last words.
Authentication is: password hashing with proper algorithms (bcrypt/argon2, not SHA-256), secure session management, CSRF protection, OAuth2 flows for social login, email verification, password reset flows, rate limiting on login endpoints, MFA support, and about 15 other things you'll discover the hard way.
Just use a provider:
| Provider | Free tier | When to pick it |
|---|---|---|
| Clerk | 10k MAU | Best prebuilt components, great with React/Next.js |
| Supabase Auth | 50k MAU | Already using Supabase? Use this. |
| Auth.js | Free (self-hosted) | Want full control, comfortable managing it |
| Firebase Auth | Generous | Need broad platform support |
With Clerk, adding auth to a Next.js app is literally:
// middleware.ts
import { clerkMiddleware } from "@clerk/nextjs/server";
export default clerkMiddleware();
export const config = {
matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
};
That's your entire auth middleware. Sign-up, sign-in, session management, OAuth — all handled. You spent 30 seconds instead of 3 weeks.
The mistake everyone makes: Rolling custom auth because they want "full control." You don't want full control over password hashing. You want full control over your product. Let someone whose entire business is auth handle the auth.
Security: the 90% that takes 10 minutes
You don't need to be a security expert. You need to do a few things that cover almost all real-world threats.
Add these security headers to your Caddy config:
myapp.com {
reverse_proxy localhost:3000
header {
# Prevent clickjacking
X-Frame-Options "DENY"
# Prevent MIME type sniffing
X-Content-Type-Options "nosniff"
# Control referrer info
Referrer-Policy "strict-origin-when-cross-origin"
# Basic CSP — adjust for your needs
Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"
# Enforce HTTPS
Strict-Transport-Security "max-age=31536000; includeSubDomains"
# Don't leak server info
-Server
}
}
That's 80% of your security headers done. The rest is common sense:
-
Don't store secrets in code. Use environment variables or a secrets manager (Doppler, Infisical). If your
.envfile is in your git repo, stop reading this and go fix that right now. -
Keep dependencies updated.
npm audit,dependabot, whatever your ecosystem provides. Most real-world breaches come from known vulnerabilities in outdated packages. - Validate input on the server. Client-side validation is for UX. Server-side validation is for security. A library like Zod makes this painless:
import { z } from "zod";
const CreateUserSchema = z.object({
email: z.string().email().max(255),
name: z.string().min(1).max(100),
// no .optional() on purpose — require what you need
});
// In your route handler
const result = CreateUserSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ errors: result.error.flatten() });
}
// result.data is typed and validated
This stuff takes 10-15 minutes and blocks the vast majority of attacks that actually happen to small apps. Nobody's running a zero-day against your SaaS with 50 users. They're trying SQL injection on your login form. Do it now, not "later." Later is when the breach happens.
GDPR: yes, this applies to you
If you have any European users (and you do — the internet is global), GDPR applies from user #1. It's not a "big company" thing.
The minimum viable compliance:
- Privacy policy. What data you collect, why, how long you keep it, who you share it with. Use a generator like Iubenda or Termageddon to get started, then customize.
- Cookie consent. If you use analytics or any third-party scripts that set cookies, you need a banner. Not the annoying "we use cookies, OK?" kind. An actual one that lets users opt out.
- Data access/deletion. Users can request their data and ask you to delete it. You need a way to do this. At small scale, it can be a manual process. Just document how.
- Data processing records. Know what data you collect, where it's stored, and who processes it. A simple spreadsheet works.
It sounds like a lot. It's not. It's a weekend of setup, mostly writing text and configuring a cookie banner. The fines for non-compliance are up to €20 million or 4% of annual global revenue. For a startup, that's existential.
CI/CD: push to main, it deploys
Manual deployments are a trap. They're fine on day one. By day thirty, you're SSHing into production, running git pull, restarting the process, and praying nothing broke. Every manual step is a chance for human error.
Here's a GitHub Actions workflow that deploys to a VPS on every push to main:
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy to VPS
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
script: |
cd /opt/myapp
git pull origin main
npm ci --production
pm2 restart myapp
Four secrets in your GitHub repo settings, one YAML file, and you never SSH into production to deploy again. Push to main, it deploys. That's the entire workflow.
Want tests to run first? Add a test job before the deploy (same structure as the job above, just with npm ci && npm test as the steps, and make deploy depend on it with needs: test). Now you have CI and CD. Tests run on every push, deploy only triggers on main if tests pass. Production-grade pipeline for $0.
Monitoring: know before your users do
Your app will crash. It's not a question of if. The question is whether you find out from your monitoring or from an angry tweet.
Two things to set up on day one:
Uptime monitoring. Something that pings your app every minute and alerts you when it's down. UptimeRobot does this for free. BetterStack if you want something nicer. Two-minute setup.
Error tracking. Sentry catches every unhandled exception with a full stack trace, the request that caused it, and the user who hit it.
// sentry.ts — one-time setup
import * as Sentry from "@sentry/node";
Sentry.init({
dsn: process.env.SENTRY_DSN,
tracesSampleRate: 0.1, // 10% of requests get performance traces
environment: process.env.NODE_ENV,
});
Sentry's free tier gives you 5,000 errors per month. If you're hitting 5,000 errors a month, you have bigger problems than your Sentry plan.
The worst version of this: no monitoring at all. You find out about downtime when a user emails you. Or you never find out, and they just leave.
The budget reality
All of this sounds like a lot. But here's the cost breakdown:
| Budget | What you get |
|---|---|
| $0/mo | Vercel + Supabase + Clerk + Cloudflare free tiers. A real app, deployed, with auth and a database. |
| $10/mo | Hetzner VPS + Caddy + Postgres + Cloudflare + automated backups. Real infrastructure you control. |
| $50/mo | Add managed database (Neon/Supabase Pro) + Sentry + Cloudflare Pro. You stop managing things and focus on building. |
| $200/mo | Staging environment + BetterStack monitoring + secrets management + bigger server. Team-ready production. |
The blocker was never money. It was knowing what to set up and in what order.
This is the surface
Every section in this post deserves its own deep dive. Which provider to pick at each stage, how the decision changes depending on whether you're building SaaS vs e-commerce vs a marketplace, what the migration path looks like when you outgrow your current setup.
I spent 8 months writing all of that into a book called Ship It. 306 pages, 32 chapters. It covers the exact hosting, payment, security, compliance, and deployment setup for startup founders at every budget tier. No affiliate links, no vendor bias. Opinionated advice from someone who's made the mistakes.
The hardcover is available now. Ebook and paperback are coming in a few days.
Stop letting it sit on localhost
The code was never the hard part. That was true 5 years ago, and it's even more true now that AI writes most of it. The skills that actually matter: picking infrastructure, setting up payments, handling compliance. They're learnable, finite, and mostly a one-time setup.
You don't need to become a DevOps engineer. You need this checklist and a weekend.
What's blocking you right now? Drop it in the comments. I've probably hit the same wall and can point you in the right direction.

Top comments (0)