What happens when auth meets money - and what I learned building it
Shipping AuthShield felt good for about a day.
The system worked. JWT issuance, refresh token rotation, OAuth integration, role-based access control, rate limiting - everything I'd set out to build was there. I'd documented every phase, explained every tradeoff, written about every decision that mattered. By any reasonable measure, it was done.
But something kept nagging at me.
AuthShield could tell you exactly who someone is. It could verify their identity in milliseconds, check their permissions, block them if they were hitting endpoints too hard. The auth layer was as tight as I could make it.
And then I asked myself a question I hadn't asked before.
What are they actually doing once they get in?
The Gap Between Identity and Action
Authentication answers "who are you." Authorization answers "what are you allowed to do." But neither of those answers the question that actually matters under pressure - what happens if something goes wrong while you're doing it?
In most applications, a failed operation is annoying. A user retries, the system recovers, nobody loses anything permanent. But there's a class of application where a failed operation isn't just an error. It's a liability.
Financial systems.
Think about a peer-to-peer transfer. The workflow looks deceptively simple:
- Validate the request
- Check the sender's balance
- Debit the sender
- Credit the receiver
- Log the transaction for audit
Five steps. If all five complete, everything is fine. But what if the system crashes after step 3? Money has left the sender's account. The receiver has nothing. There's no audit record. The database is now in a wrong state - and depending on how your system is built, you might not even know it happened.
Auth had nothing to say about this. AuthShield could verify the person initiating that transfer was exactly who they claimed to be, with exactly the right permissions to do it. But once they were in, what happened inside the room was completely uncharted.
That gap is what I wanted to understand.
Why I Built VaultPay
I didn't set out to build a payments company. VaultPay is a production grade portfolio project - but I gave myself constraints that would force me to learn the hard version of each problem, not the toy version. The kind of constraints real financial systems live with.
No partial failures. Every transfer either completes fully or rolls back entirely. Debit, credit, audit log - all atomic. If one step fails, the whole operation fails cleanly and a record of the failure is preserved separately.
No silent data loss. If something breaks, there is a record of what broke, why, and when. Not a vague 500 error. A structured, queryable audit trail.
No auth reinvention. AuthShield handles identity. VaultPay trusts it completely, validates JWTs locally for standard requests, delegates to AuthShield for sensitive operations, and focuses entirely on what it's actually responsible for - moving money reliably.
That last constraint was the most deliberate one. One of the things building AuthShield taught me is that auth done well is a solved problem if you respect the boundaries. The mistake most systems make is coupling auth logic into every service that needs it - writing JWT parsing code in three different places, handling role checks inconsistently, ending up with auth debt scattered across the codebase.
VaultPay doesn't do that. The client authenticates with AuthShield and receives a JWT. That token travels with every request to VaultPay via the Authorization header. VaultPay validates it locally using the signing key, extracts the user role, and makes its authorization decision. AuthShield is consulted again only when the operation is sensitive enough to warrant verifying the token's current state - not just its signature.
Clean separation. Each service owns exactly what it should and nothing more.
What Building AuthShield Made Possible
There's a version of VaultPay I could have built without the AuthShield foundation, and it would have been a mess.
It would have had auth logic scattered through transaction handlers. Role checks duplicated in three places. No clear line between "is this person who they say they are" and "can this operation complete safely." And it probably would have gotten consistency wrong in subtle ways that only surface under failure conditions — exactly the scenarios that matter most in financial systems.
Building AuthShield first changed how I think about system boundaries. When you spend months thinking carefully about what auth is responsible for and what it explicitly isn't, you start applying that same discipline everywhere. Every component should know what it owns. Every interface should be narrow and deliberate.
That clarity is what made the hard parts of VaultPay tractable. When you're not fighting auth concerns in the middle of transaction logic, you can think clearly about atomicity. When your identity layer is stable and trusted, you can focus on the consistency guarantees that actually protect your users.
What I Kept Running Into
Five questions kept coming up while building this. I'll go deep on each in the posts that follow, but here's the shape of them:
What happens if a transfer fails at step 4 of 6? This is the consistency problem. The approach I landed on was simpler than I expected going in - a single atomic database transaction wrapping every step. On failure, everything rolls back. Failed operations get recorded separately so there's always an audit trail, even for things that didn't complete. The interesting part was figuring out mid-transfer edge cases — like what happens if a wallet gets frozen while a transfer is already in flight.
How do you block an unknown IP without making every login painful? Pure blocklists are blunt instruments. What I ended up building was a trust system - new IPs get a 30-minute hold and an email confirmation flow, trusted IPs get sub-millisecond Redis checks on every request. The UX tradeoff took longer to think through than the implementation.
How do you know a request you've already processed is arriving again? Idempotency. Most tutorials skip this entirely but it's the thing that prevents double-spends. Getting it right meant understanding what "already processed" actually means at the database level, not just the application level.
How do you store someone's identity documents without becoming a security liability? KYC forced me to think about encryption in a way auth never did - AES-256 for storage, SHA-256 for duplicate detection, and a completely different set of access controls for who can read what.
How do you enforce permissions across two services that share no database? The 4-tier RBAC spanning AuthShield and VaultPay. The JWT carries what VaultPay needs, but knowing exactly when to trust the token versus verify it fresh with AuthShield turned out to be the real design decision.
What's Coming in This Series
Each post starts with the question. Then follows the journey - what I tried first, where that broke, and what I actually learned.
- Phase 1: What happens if a transfer fails at step 4 of 6? - the atomic transfer engine, rollback design, and mid-freeze detection
- Phase 2: How do you block an unknown IP without making every login painful? - the IP trust system and why Redis TTLs replaced what I thought needed a cron job
- Phase 3: How do you know a request you've already processed is arriving again? - idempotency keys and what double-spend actually looks like in practice
- Phase 4: How do you store someone's identity documents without becoming a liability? - KYC encryption, duplicate detection, and what changes after approval
- Phase 5: How do you enforce permissions across two services that share no database? - cross-service RBAC and when dual-mode JWT validation is about correctness, not just performance
Financial systems are the highest-pressure test I know for backend engineering. Identity is table stakes. Consistency under failure is where things get genuinely difficult - and genuinely interesting.
AuthShield was the lock.
VaultPay is the vault.
Let's get into it.
Top comments (0)