Nobody signs up for Drawline because of its login page. But an app that connects to your exchange account, even with read-only keys, has to earn trust before it's allowed to show you anything interesting. So milestone 1 was the foundation: accounts, roles and permissions, sign-in, two-factor, sign-up, and email verification.
Here's how it went, including the part that nearly shipped broken.
Scaffolding first
Before accounts came the skeleton. It's a monorepo with a web app, a server, and two shared packages. One of those packages, rules, is where the engine lives, and it has lint rules that keep it pure: no database, no network, no clock it didn't get handed. Money uses a decimal type with 40 digits of precision and banker's rounding, because a trading tool that rounds a fee wrong by a fraction of a cent, a few thousand times over, is a trading tool you can't trust.
The first real endpoint was a health check that reports whether the server is up and whether it can reach the database. Small, but it proves the whole chain works end to end.
One setup wrinkle worth recording: installing PostgreSQL on Windows through winget failed with a 403, so it went in by direct download instead. I wrote that workaround down straight away, because future me will absolutely hit it again.
Accounts, done carefully
The accounts database landed as ten tables, including an append-only audit log. Staff actions get written there and can never be edited afterwards. On top of that sits role-based access control. Every route asks for a permission, and there's a guard against removing the last admin.
A few of the decisions:
- Passwords are checked against known breaches when you set or reset one, so a password that has already leaked somewhere else gets turned away.
- Sign-up doesn't reveal whether an email is taken. You get the same answer either way, so the form can't be used to find out who has an account.
- Sign-up is 18+, asking for birth year only, not a full date of birth I don't need.
- Sessions slide for 30 days, with a hard cap at 90. Staff sessions are shorter and require 2FA.
To test all the email flows without sending real mail, I built a dev-only inbox. Verification links and reset emails show up on a page inside the app, but only in development. The server also flat out refuses to start in production mode until real email delivery exists. I'd rather it fail loudly than quietly swallow everyone's verification emails.
The bug
Two-factor authentication went in next: authenticator-app codes, ten recovery codes, and a pending step during login.
And the tests caught something that would have been a disaster.
The TOTP secrets were being read out of a Node Buffer in a way that pulled from Node's shared memory pool. Small Buffers in Node are carved out of one larger preallocated slab. If you read the underlying memory without respecting the Buffer's offset and length, you get someone else's bytes. The secret my code thought it was using wasn't the secret your phone had.
The result would have been that every real 2FA code gets rejected. Anyone who turned on two-factor would have locked themselves out on their very next login.
It never reached a human. The test suite caught it before my own walkthrough of the feature. That's the whole argument for writing tests on the boring parts, in one bug: the failure lived in a detail of how Node manages memory, not in anything the 2FA code looked like it was doing wrong. Reading that code, I'd have sworn it was fine.
The little things
Two details I'm fond of:
- A rotating quote beside the login form. There are 52 of them, it never repeats the one you just saw, and it fades in (or doesn't, if you've asked your OS for reduced motion). No authors attached, on purpose. The line should stand on its own.
- Settings you'd actually want. You can see every device you're signed in on, sign out one or all the others, change your password, and move 2FA to a new phone. Staff can't turn 2FA off at all. And for the case where someone loses their phone and their recovery codes, there's a server-only reset command that's audit-logged.
By the end of the milestone there were 104 tests, and the foundation was done. Now the actual product could start: the rule engine.
~ Jer