DEV Community

Launchstack310
Launchstack310

Posted on • Originally published at launchstack.space

What Nobody Tells You About Building a SaaS: Auth, Payments, Emails and Infrastructure

Building a SaaS is fun… until you reach the boring parts.
Auth. Payments. Emails. Edge cases. Infrastructure.
These are the things that turn a weekend project into weeks of work. Most founders start a SaaS thinking about the product. But the real complexity often comes from everything around it.
Here's what nobody tells you.

  1. Authentication Is Never Just "Login" Everyone thinks auth is a checkbox. It's not. You need:

Email/password login with proper password hashing (bcrypt, not MD5)
OAuth (Google, GitHub) — because users don't want another password
JWT tokens with refresh logic — access tokens expire, and you need to handle that gracefully
Password reset via email with secure, time-limited tokens
Session management — what happens when a user logs in from two devices?
Rate limiting on login endpoints to prevent brute force attacks

In Flask, a minimal JWT setup looks like this:
pythonfrom flask_jwt_extended import JWTManager, create_access_token, jwt_required

jwt = JWTManager(app)

@app.route('/login', methods=['POST'])
def login():
user = verify_credentials(request.json)
access_token = create_access_token(identity=user.id)
return jsonify(access_token=access_token)

@app.route('/protected')
@jwt_required()
def protected():
return jsonify(message="you're in")
Simple. But that's 5% of the work. The other 95% is the edge cases: expired tokens, invalid tokens, revoked tokens, CORS issues in production, OAuth callback URLs, email verification flows...
Most developers spend 1-2 weeks just on auth. And that's before writing a single line of their actual product.

  1. Payments Are Where Things Get Expensive to Get Wrong Stripe is the standard. But wiring it up correctly takes more time than people expect. The basics:

Create a checkout session
Handle the payment success redirect
Store the subscription status in your database

The part that actually matters:

Webhooks — Stripe sends events asynchronously. If your server is down for 10 minutes, you miss them.
Signature verification — anyone can send fake POST requests to your webhook endpoint if you don't verify the Stripe signature
Idempotency — the same webhook event can arrive multiple times. Your handler needs to be idempotent.
Failed payments — what happens when a user's card is declined? Do you downgrade them immediately? Send an email? Give a grace period?
Subscription changes — upgrades, downgrades, cancellations mid-cycle. Proration logic is not fun.

This is the Stripe webhook handler you actually need in production:
python@webhooks_bp.route('/stripe', methods=['POST'])
def stripe_webhook():
payload = request.data
sig_header = request.headers.get('Stripe-Signature')

try:
    event = stripe.Webhook.construct_event(
        payload, sig_header, endpoint_secret
    )
except ValueError:
    return jsonify({'error': 'Invalid payload'}), 400
except stripe.error.SignatureVerificationError:
    return jsonify({'error': 'Invalid signature'}), 400

if event['type'] == 'checkout.session.completed':
    handle_checkout_completed(event['data']['object'])
elif event['type'] == 'customer.subscription.deleted':
    handle_subscription_cancelled(event['data']['object'])

return jsonify({'status': 'ok'})
Enter fullscreen mode Exit fullscreen mode

Skip the signature verification and you're vulnerable. Skip the idempotency and you'll double-charge users. Skip the failed payment handling and you'll lose revenue silently.
Most developers spend another 1-2 weeks on payments. At minimum.

  1. Emails Are a Reliability Problem, Not a Code Problem Sending an email in Python is three lines of code. Sending emails reliably is a different problem. You need transactional emails for:

Email verification on signup
Password reset
Payment confirmation
Subscription renewal reminder
Failed payment notification
Account deletion confirmation

The code is simple with a service like Resend:
pythonimport resend

resend.api_key = os.getenv("RESEND_API_KEY")

def send_purchase_confirmation(to_email, user_name):
resend.Emails.send({
"from": "LaunchStack noreply@launchstack.space",
"to": to_email,
"subject": "Your purchase is confirmed",
"html": f"

Hey {user_name}, you're all set.

"
})
But the real work is:

HTML email templates that look good on all clients (Gmail, Outlook, Apple Mail)
Handling bounces and unsubscribes
Not ending up in spam
Making sure emails actually send when your server is under load

Another few days of work, at minimum.

  1. The Edge Cases Nobody Thinks About
    These are the things that will break your app at 2am six months after launch:
    User deletes their account — do you hard delete or soft delete? What happens to their data? Their subscription? Their invoices?
    Refunds — a user wants their money back. How does that flow through your system? Does it automatically cancel their subscription?
    Concurrent requests — two requests hit your server at the same time and both try to update the same user record. Race condition.
    Database migrations — you need to add a column to a table that has 50,000 rows. Your app can't go down while this runs.
    Token expiry edge cases — user has the app open in two tabs. One tab refreshes the token. The other tab tries to use the old token. What happens?
    None of these are hard to solve individually. But each one takes time to think through, implement, and test.

  2. Infrastructure Is the Tax You Pay to Ship
    You have a working local app. Now you need to deploy it.

Hosting — Vercel for the Next.js frontend is easy. Flask backend needs a server: Railway, Render, Fly.io, a VPS.
Database — local SQLite is fine for development. Production needs PostgreSQL, proper backups, connection pooling.
Environment variables — you have 15 env vars. Where do they live? How do you manage them across dev, staging, and production?
CORS — your frontend at yourdomain.com can't talk to your backend at api.yourdomain.com until you configure this correctly.
SSL — HTTPS everywhere. Let's Encrypt is free but you need to set it up.
Monitoring — how do you know when your app is down? When an error happens?

This is the CORS config that actually works in production with Flask:
pythonCORS(app, resources={
r"/": {
"origins": "
",
"methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
"allow_headers": ["Content-Type", "Authorization"],
"supports_credentials": True,
"max_age": 3600
}
})
Getting infrastructure right is another week of work. Maybe two.

The Real Timeline
Here's what building a SaaS actually looks like for most developers:
TaskExpectedRealityAuthentication2 days1-2 weeksPayments1 day1-2 weeksEmails2 hours2-3 daysInfrastructure1 day1 weekEdge cases—Ongoing
That's 4-6 weeks before you've written a single line of your actual product.

Why SaaS Boilerplates Exist
This is exactly why SaaS boilerplates exist.
Instead of spending weeks setting up authentication, payments, and infrastructure, many founders start from a pre-built foundation that already handles all of this correctly.
That's actually why I built LaunchStack — a production-ready Next.js + Flask boilerplate for Python developers. It comes with JWT auth, Google OAuth, Stripe payments with webhook handling, transactional emails via Resend, and a complete deployment setup.
The goal is simple: skip the boring parts. Start with everything already working. Ship the product you actually want to build.
If you're a Python developer who wants to launch a SaaS without spending weeks on setup, check it out.

Top comments (0)