DEV Community

Souvick Sarkar
Souvick Sarkar

Posted on

I Built a Production-Grade Auth API From Scratch — Here's Everything I Learned

Most tutorials teach you authentication in 15 minutes. They show you jwt.sign(), slap a middleware on a route, and call it a day.

Real authentication systems are nothing like that.

I spent 5 weeks building Swaraksha — a production-grade authentication API that handles everything a real system needs: JWT token pairs, refresh token rotation with theft detection, TOTP-based multi-factor authentication, role-based access control, account lockouts, rate limiting, and a fully automated deployment pipeline.

This article is the deep dive. I will walk you through every feature, every design decision, and every mistake I made along the way.

GitHub: Repo Link
Live Swagger Docs: Demo Link

The Tech Stack
Before we get into the code, here is what I used and why:

Part 1: Authentication That Actually Works

The Problem With Single Tokens
Most tutorials give you one JWT token. The user logs in, gets a token, and uses it for everything. But this creates a horrible tradeoff:

  • Short-lived token (15 minutes)? The user has to log in again every 15 minutes. Terrible UX.
  • Long-lived token (7 days)? If someone steals it, they have full access for a week. Terrible security.

The Solution: Token Pairs
Swaraksha uses two tokens:

  1. Access Token (15 minutes) — Used for every API request. Short-lived, so even if stolen, the damage window is tiny.
  2. Refresh Token (7 days) — Stored securely by the client. Used only to get a new access token when the old one expires.

Refresh Token Rotation (Theft Detection)
Here is where it gets interesting. Every time a client uses a refresh token to get a new access token, I immediately invalidate the old refresh token and issue a brand new one.

Why? Imagine this scenario:

  • A hacker steals your refresh token.
  • You use your (original) refresh token to get a new access token.
  • The server gives you a new token pair and marks the old refresh token as "used."
  • The hacker tries to use the stolen (now "used") refresh token.
  • The server detects reuse of a consumed token → Token Theft Detected!
  • The server instantly revokes the ENTIRE token family, logging out both you and the hacker.

This is called Automatic Reuse Detection, and it is the industry standard used by Auth0 and Okta.

Part 2: Multi-Factor Authentication (MFA)
Adding a password is good. Adding a second factor is better.

Swaraksha implements TOTP (Time-Based One-Time Password) — the same protocol used by Google Authenticator and Authy.

How It Works

  1. Setup: The server generates a secret key and returns it as a QR code URI.
  2. Enable: The user scans the QR code in their authenticator app and sends back a 6-digit code to prove it works.
  3. Login: After entering their password, users must provide the current 6-digit code from their authenticator app.

The beauty of TOTP is that the server never needs to send a code to the user. The authenticator app and the server independently generate the same code based on the shared secret and the current time. If they match, the user is verified.

Part 3: Role-Based Access Control (RBAC)
Not every user should have the same power. Swaraksha has three roles:

The authorize Middleware
The key insight is separating authentication (who are you?) from authorization (what are you allowed to do?).

Now protecting a route is a single line:

Part 4: Defensive Programming

Account Lockouts
Brute-force attacks try thousands of passwords per second. To stop them, Swaraksha tracks failed login attempts:

  • 5 failed attempts → Account is locked for 15 minutes.
  • Every failed attempt increments a counter in the database.
  • A successful login resets the counter to zero.

Rate Limiting with Upstash Redis
Even with account lockouts, an attacker could target thousands of different accounts. Rate limiting stops this by restricting requests per IP address:

  • Login routes: 5 requests per 15 minutes per IP
  • API routes: 100 requests per minute per IP

Zod Validation at the Framework Level

Instead of manually checking if (!email || !password) in every route, Swaraksha uses Zod schemas that Fastify automatically enforces:

Part 5: Containerization with Docker

Why Docker?
"But it works on my machine!" — Every developer, ever.

Docker packages your entire application — Node.js version, dependencies, compiled code — into a single, portable image. It will run identically on your laptop, your teammate's laptop, and a cloud server in Singapore.

Multi-Stage Builds
A naive Dockerfile would install TypeScript, compile the code, and ship everything — including the compiler. That is wasteful and insecure.

Swaraksha uses a multi-stage build:

Part 6: CI/CD — From Push to Production
This is the part that makes everything feel like magic.

The Pipeline
Every time I push code to main, GitHub Actions automatically:

  1. Checks out the code on a fresh Ubuntu server
  2. Installs dependencies and caches node_modules
  3. Runs TypeScript compilation to catch type errors
  4. Runs the entire Vitest test suite to catch logic errors
  5. Builds the Docker image to catch packaging errors
  6. Pushes the image to GitHub Container Registry
  7. SSHs into my DigitalOcean Droplet and pulls the new image
  8. Runs database migrations in a disposable container
  9. Swaps the live container with the new one

The Migration Deadlock
I hit an interesting bug during deployment. My original pipeline was:

  • Start the container
  • Wait for it to be running
  • Run database migrations inside the container

*The problem? *
The Fastify server tried to connect to the database on startup. If the database schema was not migrated yet, the server crashed instantly. I could never reach step 3 because the container died at step 1.

The fix: Run migrations in a temporary, disposable container before starting the real server:

Part 7: Infrastructure & Security

Server Hardening
The DigitalOcean Droplet runs Ubuntu with:

UFW Firewall — Only ports 22 (SSH), 80 (HTTP), and 443 (HTTPS) are open. Port 3000 (Node.js) is blocked from the outside world.

SSH Key Authentication — Password login is disabled. Only cryptographic keys can access the server.

Non-root Docker User — Even if someone exploits the Node.js app, they cannot escalate to root.

Caddy Reverse Proxy
The Node.js server is never directly exposed to the internet. A Caddy reverse proxy sits in front of it:

Internet → Caddy (port 80) → Node.js (port 3000, internal only)

Caddy handles all incoming traffic, protects against malformed requests, and forwards legitimate traffic to the Node.js app over Docker's internal network.

Secrets Management
No secret is ever committed to Git. Environment variables are stored as GitHub Secrets and injected into the server's .env file during deployment by the CI/CD pipeline.

Part 8: Observability
A deployed app without monitoring is a ticking time bomb. Swaraksha includes:

Sentry (Error Tracking)
Every unhandled exception is automatically captured and sent to Sentry with the full stack trace

Health Endpoint
A simple /health endpoint returns the server's uptime and timestamp. Uptime monitoring services (like UptimeRobot) ping this every 5 minutes and email me if the server goes down.

What I Learned

Authentication is never "done." There is always another attack vector to consider — token theft, brute force, replay attacks, timing attacks.

CI/CD is not optional. Manual deployments are error-prone. Automating the pipeline saved me hours and eliminated "it worked on my machine" bugs.

Docker multi-stage builds are essential. Shipping development tools to production is a security risk and a waste of resources.

Database migrations in production are scary The migration deadlock taught me to always migrate before the app starts, never after.

Monitoring is as important as coding. If your server crashes and nobody knows, did it really crash? Yes. Yes it did.

Try It Yourself
GitHub: github.com/SouvickSarkar20/Swaraksha
Live Swagger Docs: http://168.144.117.195/docs

Thanks for reading. If you have questions or spot something I could improve, drop a comment below!

Top comments (0)