Trim the stack
40 swaps for auth, storage, infra, and libraries
Problem: Things are too complex
Before coding agents, a PR went through more checkpoints. You’d suggest a design pattern in an RFC or an issue comment. Your teammate would advocate for a different pattern. You’d go back and forth on the idea before committing to a tradeoff. After a day of POCing the first approach, you realized there’s an easier way, so you’d update the RFC or have another conversation. Then you’d submit a draft PR. More feedback from teammates who skimmed it with fresh eyes. They asked for more tests and told you how you could ditch that new library for a built-in. More refinement. By the time you make the PR public and everyone approves, your code is 2X simpler than the first version.
Things are different now.
Instead of clarifying our thinking in an RFC or issue, we prompt and get a PR. An agent reviews the code and auto-fixes major bugs. Another writes the PR description.
The end.
Sure, there’s a little back-and-forth between you and the agent, but only enough to make the tests green. Even reading the code is starting to feel like an inconvenience.
Our velocity has skyrocketed, but so has the complexity of our codebases. Part of that complexity is inevitable. Yet more is a byproduct of accepting the agent’s defaults without scrutiny. The agent doesn’t want to waste tokens refining its already green solution, and we don’t want to waste time nitpicking. So extra libraries get added, nested if statements sneak in, and kludgey abstractions get overlooked.
Teams won’t care until their complexity debt starts to slow their velocity or introduce more bugs. At that point, it’ll be tempting to start from scratch with another prompt. But there might be a more pragmatic way out.
Solution: Simpler tools
Replace complex tools with simple tools. Keep the requirements and tests, but replace one heavy tool with a lighter one.
To make this practical, here are 40 swaps to consider, organized by domain.
Simpler Auth
The worst enemy of security is complexity
— Bruce Schneier, Cryptographer.
Auth0 / Clerk →
express-session+ bcrypt + auserstable. Don’t lock yourself into a vendor for a personal project.OAuth → API keys. Trade the redirect dance with an API key in the header. Consider OAuth when your users can only auth with accounts they don’t control (rare).
Username / password → Magic link. No hashing, strength enforcement, or reset forms.
Per-route auth middleware → Single auth gateway. A single middleware that authenticates every request by default is easier than sprinkling
requireAuth()on each route.SSO → anything else. Add it when a paying customer asks for it.
JWT everywhere → Server sessions. Easier to revoke and doesn’t require rotation.
RBAC service →
rolecolumn on auserstable. Unless you’re an enterprise,WHERE role = ‘admin’in your middleware is fine. Consider a better abstraction when you have 3+ distinct roles.Third-party MFA service → TOTP +
otplib. It’s an open spec, andotplibgenerates and validates codes in ~10 lines. There’s a maintenance burden, but it might be cheaper than locking yourself in too early.Refresh token rotation → long-liven tokens + revocation list. If calls are inside your infra, you might not need to worry about refreshing yet.
passport.js→ manual middleware. Passport adds 20+ files and a plugin ecosystem just to check a password and set a session. If you're not juggling multiple strategies, averifyPasswordfunction and a session check is 15 lines and nothing to maintain.
Simpler Storage
Adding technology to your company comes with a cost. As an abstract statement this is obvious: if we’re already using Ruby, adding Python to the mix doesn’t feel sensible […]. But somehow when we’re talking about Python and Scala or MySQL and Redis people lose their minds, discard all constraints, and start raving about using the best tool for the job
— Dan Mckinley, Principal Architect @ Etsy
Redis queues → Postgres
LISTEN/NOTIFY. Postgres has a built-in pub/sub mechanism.NOTIFYsends a message on a named channel;LISTENopens a persistent connection that receives those messages in real time. You can store messages in ajobstable and tell workers to pick it up onNOTIFY. Or use a lib like pg-boss if you don’t wanna coordinate locks and retries yourself. This’ll last you until you get to a few hundred jobs / second.Trendy DB → SQLite. A single file, zero config, and it handles tens of thousands of reads per second. Upgrade to Postgres when you need concurrent writes from multiple processes.
Time-series DB (InfluxDB) → Postgres
timestamptz+ indexes. For application metrics, user activity logs, or event streams at normal SaaS scale, a timestamptz column with a partial index covers the query patterns without running a second database.Separate search service (Algolia, Elasticsearch) → Postgres full-text search.
ORMs (Prisma, Sequelize) → SQL + a query builder. OMRs abstract SQL until they don’t. If you’ll be writing raw queries anyway, a lightweight query builder can get the job done without hiding the DB from you.
MongoDB → Postgres JSONB. If you’re already on Postgres and don’t want to stress about schema design upfront, you can still use JSON for now without having to bring in another DB. IF your data is document-shaped AND you’re confident you’ll hit scale quickly, then ignore.
Redis cache → Postgres Materialized View. Pre-compute the result and store it in a table. Great way to preserve speed for complex queries and avoid a cache. Must be OK with some data staleness.
Use-case: daily sales dashboard.
See: Choosing Between Postgres Materialized Views and Redis Application Caching (leapcell.io).
S3 → local filesystem. If you’re on a single VPS, you don’t need to manage another round-trip, IAM policy, and subscription. If GitLab could run its
gitcommands on a shared NFS filesystem, you’re probably fine to store some files locally.GitLab Architecture (fullstack.zip)
Multiple databases → one Postgres schema per domain. Running separate databases for separate domains means cross-domain queries require application-level joins, separate connection pools, and separate migration pipelines. A Postgres schema per domain (
users.accounts,billing.invoices) gives you logical separation with a single connection string.Key-value store → single-table Postgres with a
typecolumn. Redis shines for ephemeral data — rate limiting, leaderboards, session tokens. For “store this thing and look it up by key” use cases in a persistent store, a Postgres table with(type, key, value, expires_at)is queryable, inspectable, and doesn’t require operating a second data store.
Simpler Infra
The solution to this is to not solve all the problems a billion dollar tech does on a personnal project. Let it not be idempotent. Let it crash sometimes. We lived without kubs for years and the web was ok. Your users will survive.
— BiteCode_dev on HN
Fly.io / Railway → 1 VPS. SSH in, run your app with
pm2, put Caddy in front. You own the machine, you understand the machine, and you're not beholden to a platform's pricing or uptime decisions.k8s →
systemd+ reverse proxy. Automatic startup, dependency checking, and failure recovery.How to run pods as systemd services with Podman | redhat.com
Replacing Kubernetes with Systemd | news.ycombinator.com.
Docker → Bash +
pm2. Replace the whale withpm2(for Node apps), and you’ll free up a ton of RAM, storage, dependencies, and time.Terraform → Bash. For a personal project or small team with one VPS, a shell script that runs apt install, creates users, and drops config files is fine. Terraform is for provisioning lots of stuff across teams.
Secrets manager → Env files +
chmod 600.Staging environment → Feature flags. It doesn’t get more real than prod, not matter how much you dress up your staging env. How many of us are acting like we have an SLA with five 9s when we don’t even have 5 customers?
GitHub Actions →
githooks + deploy script. A post-receive git hook on your server that pulls and restarts is a 10-line bash script that deploys on git push.Autoscaling → Vertical scaling. What happened first, you spent a week configuring autoscaling or 10 minutes doubling your VPS size?
Nginx + Certbot → Caddy. Auto-TLS, one config file. HTTPS in Nginx usually requires installing Certbot separately, configuring it, setting up a cron job for renewal, and debugging when it gets out of sync with your Nginx config. With Caddy, you just put your domain name in the Caddyfile — Caddy obtains and renews the certificate automatically, handling HTTP-to-HTTPS redirects, OCSP stapling, and multi-domain SANs with no additional configuration. The makers of Certbot even agree that this is a better solution for everyone.
Multi-region deployment → backups + a restore script. Small SaaS apps don’t need 99.99% uptime. A daily Postgres dump to S3 and a restore script that gets you back in 10 minutes is more than enough.
Simpler Libraries
All non-trivial abstractions, to some degree, are leaky
— Joel Spolsky, Co-Founder @ StackOverflow
ESLint + Prettier + Husky →
biome. Biome replaces that entire stack with one tool — it’s 10–25x faster than ESLint and ships as a single binary instead of 127+ npm packages. Caveat: it doesn’t cover every ESLint plugin (e.g.eslint-plugin-jsx-a11y), so teams with heavy custom rule sets may not get a clean 1:1 swap.Jest/Vitest + mocking libs →
node:assert. Ideal for simple backend unit tests in pure Node.js environments where minimal dependencies are a priority (nodejs.org).env+dotenvlibrary → Native--env-fileflag. Since Node 20, you can load your env file natively.node-config→ env files.node-configis a popular library for managing environment-specific config (development.json, production.json, etc.). For most projects, a plain.envfile per environment covers the same ground with zero deps.uuid→crypto.randomUUID().Why does theuuidpackage get 91 million downloads per week whencryptois native and 4x faster? I think it’s just a better name.nodemon →node --watch. Unless you need granular ignore patterns, this simple flag is good for anyone on Node 18+.axios →fetch.Works identically in browsers. Can upgrade if you need interceptors or fancy errors.moment.js→DateorTemporal. Ditch the bulky libs for a lib that the JS ecosystem spent a decade pushing through. (You’ll need a polyfill until Safari gets its act together).lodash →array methods. Lodash predates modern JS._.map,_.filter,_.groupBy,_.flattenare all native today. Another example of muscle-memory keeping a heavy lib around.express→ v0:node:http,v1:hono. Nodefor simple internal services,honoif you need a router on the edge. 0 deps.
Conclusion
It’s easy for me to tell you to “just replace this whole part of your stack.” It’s hard to do this in a company, especially when you need to convince teammates along the way. After spending the last three months doing nothing but this type of cleanup in Compass Calendar, however, I can tell you it’s worth it. You’ll have fewer dependencies, fewer lines of code, and fewer headaches. You’ll start to understand the codebase again. Best of all, you’ll develop a deep conviction that’ll help you forever separate signal from noise:
It doesn’t have to be this complicated.
More
Simple is smart (fullstack.zip)
A Plea for Simplicity (schneier.com)
Choose Boring Technology (mcfunley.com)
77 Things to avoid in 2026: Developer edition (fullstack.zip)
The Law of Leaky Abstractions (joelonsoftware.com)


