The smallest file in our codebase is the one I would most want a new engineer to read:
/**
* Hard limits shared by the main process and the renderer.
*
* Kept in its own module (no imports) so the renderer bundle can pull the same
* numbers the server enforces, instead of drifting from a hardcoded copy.
*/
/**
* Maximum number of searches a user can monitor.
*
* Every search is scraped one after another inside a single poll cycle, so the
* count directly sets how stale the slowest search gets. At ~3-5s per search,
* 15 keeps a full cycle under a minute — beyond that the product stops being
* "you hear about it first", which is the only reason it exists.
*/
export const MAX_SEARCHES = 15;
That is the whole file. Two things about it are worth more than the line of code.
The number is derived, not chosen
Most limits in most products are round numbers somebody liked. 10, 50, 100. They are chosen first and justified afterwards, which is why nobody can ever tell you whether raising one is safe.
This one runs the other way. The chain is:
- Searches are scraped sequentially within a cycle (deliberately — concurrent hits on one host is a loud traffic pattern).
- A scrape takes 3–5 seconds: real page load, real browser, real network.
- So a full cycle costs roughly
count × 4s. - The product's single claim is that you hear about a listing before other applicants do. That claim dies somewhere around a minute of staleness.
- 60 ÷ 4 ≈ 15.
Now the number is falsifiable, and so is any change to it. Make scraping twice as fast and 30 becomes defensible. Add a slower site and 15 gets worse without anyone touching the constant. Someone asking "can we raise it to 50?" has a concrete answer — not "it feels like a lot" but "a cycle would take three and a half minutes, and at that point we are an email digest, not an alert".
A limit you cannot derive is a limit you can never safely change. You end up with a number nobody dares touch because nobody knows what it was protecting.
The comment records a trade, not a fact
15 is a fact. The comment is the argument: sequential scraping, the per-scrape cost, the staleness budget, and the product claim that budget defends.
Six months later the thing that is hard to recover is never the value. It is why it is not 40. Code archaeology can tell you what a line does; it cannot tell you which of the ten plausible reasons was the actual one. Writing the derivation down is the cheapest possible insurance, and this comment is longer than the code by a factor of ten because that ratio is correct here.
Two processes, one number
* Kept in its own module (no imports) so the renderer bundle can pull the same
* numbers the server enforces, instead of drifting from a hardcoded copy.
The limit has to exist in two places at once. The UI needs it to grey out the "add search" button and explain why; the main process needs it to actually refuse. Two different runtimes, two different bundles.
The failure mode if you let them drift is specific and awful: the UI cheerfully accepts a sixteenth search and the backend silently declines to poll it. Nothing errors. The user has a search in their list that produces no alerts, and no way to discover that. Silent divergence between what the UI promises and what the system enforces is worse than a visible error, because the user's model of the system is now wrong and they have no signal telling them so.
"No imports" is the load-bearing part. A shared constants module is only importable by both sides if it drags nothing in with it. The moment it imports a Node built-in, it cannot go in the renderer bundle; the moment it imports a React type, it pollutes the main process. Keeping it import-free is what keeps it shareable, and it is worth a comment so the next person does not casually add import path from 'path' and quietly force a copy-paste back into existence.
The generalisation
When you write a magic number, write down the inequality it came from. If there is no inequality — if the honest answer is "it seemed like enough" — that is worth knowing too, and worth saying, because it tells the next person the number is free to move.
See what it buys
Fifteen concurrent searches is what one licence gets you; the pricing and what is included are at notifio.app/pricing.
While you are on that page, an unrelated thing worth poking at: the price renders in your local currency, resolved server-side from a geolocation header before the HTML is sent. Load it, then load it again through a VPN exit in another country and watch the figure change — with no flicker and no client-side swap, because the decision happens before the first byte. There is a whole other post in why that turned out to be harder than it sounds.