Let's talk
engineering

A hundred hardcoded defaults, and the cause was a permission a field rep does not have

A count across the two client applications found roughly a hundred and twenty places where the language, the timezone or the currency was written into the code as a constant. English. Coordinated universal time. One currency code, repeated in every screen that showed money.

The reflex is to treat that as sloppiness and open a ticket to clean it up. The reflex is wrong, and the cleanup would have come straight back, because every one of those constants was a developer solving the same real problem: at the moment that screen renders, the application does not know what the correct value is.

It did not know because the tenant’s locale, timezone and default currency lived on a settings record, and reading that record required an administrative permission. A field sales rep does not have it and should not have it. So a rep’s phone had no lawful way to discover the currency its own prices were quoted in.

That is not a missing feature. It is an authorization boundary drawn in the wrong place, and every hardcoded constant was a workaround for it.

Configuration has two audiences

The mistake underneath is treating “tenant settings” as one thing.

There is administrative configuration: seat limits, which modules are enabled, tax regime, financial year start, the preferences an owner edits. Reading and writing those is legitimately privileged. A rep has no business seeing the seat limit.

And there is display context: what language to render in, what timezone to interpret dates in, what currency amounts are denominated in. Every authenticated user needs all three, on every screen, to render anything correctly. There is nothing sensitive about them — they are visible in any invoice the user is already allowed to see.

Putting both behind the same permission is what created the problem. The fix was to move the display context onto the session endpoint — the call every client already makes immediately after authenticating, to learn who it is and what it may do. Three more fields on a response the client was already waiting for.

The cost was nothing measurable, because the values ride on a record the request already loads while resolving the caller’s identity and permissions. No extra query. That is worth checking before adding a field to a hot endpoint, and worth stating when it turns out to be free.

The rule: anything every client needs in order to render correctly belongs on the session response, not behind a permission. If a value is required to display a number honestly, gating it does not protect anything — it just guarantees somebody hardcodes a guess.

The window where there is no tenant

There is a state this system has that many do not, and it forced a decision worth copying.

Authentication is identity-first. A user signs in as themselves, and only then chooses which tenant to work in — because one person can belong to several. Between those two steps there is a valid authenticated session with no tenant, and therefore no tenant locale, no tenant timezone and no tenant currency.

The tempting move is to return sensible defaults in that window so clients never have to handle nulls. That is exactly what produced the hundred and twenty constants in the first place, one layer further in. A default returned by the server looks authoritative; a client has no way to tell it apart from a real value, and it will format money in it.

So the endpoint returns nulls in that window, deliberately, and the clients fall back to the device’s own locale — which is at least an honest guess about the person holding the phone, and which the person can see is theirs. A guessed tenant currency is a lie that looks like data.

Both states were verified against the running system rather than reasoned about: a tenant-scoped session returns the tenant’s three values, and an identity-only session returns three nulls. That second assertion is the one that would rot silently, so it is in a committed check rather than a one-off.

Persist it with the session, not with the cache

On mobile there was a further requirement that only shows up when you take the network away.

If the display context is fetched on each launch, then the first launch with no signal renders every date and every amount in whatever the fallback is. The app comes up, shows the cached orders, and formats them wrongly — which is worse than not showing them, because a rep reading yesterday’s figures in the wrong currency has no reason to doubt them.

So the three values are stored with the session, not in the ordinary response cache. A cold start with no connectivity restores the session and the formatting context together, and every screen formats correctly before a single request succeeds.

Screens then call a formatting helper with no arguments beyond the value. That is the part that makes the fix stick: if the helper needs the locale passed in, some screen will pass the wrong one, and there is no realistic review that catches it. Twenty-six screens on the mobile client had their constants deleted in that pass, and the way to keep them deleted is to make the correct call the shorter one.

Two bugs the change nearly created

Moving off a hardcoded universal timezone is not a free refactor, and it produced two defects that were caught during the work rather than after it.

Date-only fields — a due date, an issue date, an expected close date — are not instants. Formatting them through a timezone-aware formatter shifts them by a day in either direction depending on the offset, so the switch would have moved a batch of dates by one day the moment it landed. They needed a separate calendar formatter that does not apply an offset at all.

And a task list that grouped items into overdue and due-today was doing that arithmetic against the universal calendar rather than the tenant’s. Before the change that was consistent with everything else being universal, and therefore uniformly slightly wrong. Afterwards it was inconsistent with the dates displayed beside it, which is how it got noticed.

Both are the same underlying confusion, and it is worth naming: removing a wrong default exposes every place that was silently relying on it. That is a good outcome and it does not feel like one during the change.

Rules

Split configuration by audience before you gate it. Administrative settings are privileged. Display context — language, timezone, currency — is not, and every user needs it.

Put display context on the session endpoint. It is the one call every client makes and the one response every client already waits for.

Return null where you do not know, and let the client fall back to the device. A guessed default from the server is indistinguishable from a real value and will be formatted as one.

Persist the context with the session so a cold offline start renders correctly.

Make the argument-free call the correct one. A helper that requires the locale to be passed in will be called wrongly somewhere, and you will not find it in review.

Working on something like this?

We build this kind of software, and we staff the teams that do.

Get in touch