Everything wrong with one small form
The cabinet's "My family" form was four fields long and had a problem in every layer: you could add yourself as your own relative, it demanded an ID nobody remembers, impossible dates crashed it, errors ignored your language, the dropdown wouldn't close, and backspace didn't delete. This is the whole repair — backend rules, state-service integration, and frontend polish — of one deceptively simple form.
The cabinet has a family tab: add your relatives once, and their details are ready wherever a policy needs them. The form asked for three things — the relative's national identity number, their passport series and number — and sent them to a government data service that answers only when all three match one real person. Name, birth date, driver's licence come back from the state, verified, untypeable.
Solid design. Two problems in front of it.
The first was reported as a bug: you could add yourself as your own family member. Pick "father", enter your own documents, and there you were, your own relative. Nothing downstream was ready for that.
The second nobody reported, because it didn't look like a bug: the identity number is fourteen digits, and people don't know their relatives' by heart. They know a spouse's passport number from memory or an old photo. They know birthdays cold. The ID field is where the form went to find the physical card — or to die.
You can't be your own family
The self-add fix is the kind you want: the account's own verified ID already sits in the profile — the vehicle flow reads the same field to decide whether you own the car you're adding. Compare it with the submitted one; on a match, refuse with a clear message. Three lines, placed before the call to the government service, because that call is metered and there's no reason to pay for a lookup whose answer we already refuse.
The dropdown also offered "myself" as a relationship type — the backend's reference list is shared with a flow where insuring yourself is normal. Filtered from the options, but only at the options level: existing records that legitimately carry it still display correctly.
Trading the ID for a birthday
Then the better question: why ask for the ID at all?
The government service has two doors. The one we used takes ID plus passport. The other takes date of birth plus passport — and its answer contains the ID. Two other flows in the codebase already used that second door. Switching the family form to it means the person types a birthday they know instead of a number they don't, and the system still ends up holding the verified ID — supplied by the state instead of a human.
Is that weaker verification? Barely, and it's worth being honest about the margin. The national ID literally encodes the birth date — six of its digits are the birthday — so knowing the ID implies knowing the birthday. Both doors demand all inputs match one real person in state records. Anyone who has your passport number got it from a document that shows your birthday anyway. Same threat model, one less impossible field.
One consequence needed care: the self-block compares IDs, and the client no longer sends one. So the check moved to just after the state's answer, comparing the returned ID against the profile's. Entering your own passport and birthday is still refused — the gate just stands one step later. Duplicates are now checked twice: a free passport-based check before the paid call, and an ID-based one after it, which also catches re-adding the same person under a renewed passport.
Dates that don't exist
The birthday field opened a door the ID never had: impossible input. The date normaliser was a pattern matcher — it happily turned the 31st of February into a string the database would reject, and the person saw a generic server error where a validation message belonged.
Now the server checks that the date exists on a calendar, isn't in the future, and isn't before 1900 — before the paid lookup, so garbage costs nothing. And the form's calendar picker carries the same bounds, so the interface cannot express a request the server would refuse. The two ends agree because they enforce the same rule, each in its own language.
The error that only spoke Russian
First manual test in Uzbek: a failed lookup produced an error message in Russian. Not ours — the government service's own wording, thrown raw through every layer and onto the screen, ignoring the language the person chose.
The calculators never showed this problem, and the reason was uncomfortable: they discard backend error messages entirely and show one generic translated "not found" for every failure. Honest about language, vague about cause. The cabinet form does the opposite — it shows the server's specific message per error — which means the server must speak the user's language. So every message on this path now goes through the localized catalogue, keyed on the request's language header: required fields, duplicates, self-add, not-found. The provider's raw text is caught and replaced with our own. The one trade-off: if the state service is down, the person also reads "check your date and passport" — slightly imprecise, and retrying is the right action either way.
The select that wouldn't close
Polish turned up its own bugs. The relationship dropdown confirmed your choice with a checkmark — and stayed open. The cause took real digging: the select sits inside an HTML label, and labels forward a click anywhere inside them to their first button. Click an option, the menu closes, the label forwards a second click to the trigger, the menu reopens — all in one frame, looking exactly like "it never closed". One event modifier stops the forwarding; fixed in the component, so every future caller is safe regardless of wrapping.
On phones, the same dropdown opened downward near the bottom of the screen and ran off it, putting the last options under the browser chrome. It now measures the space around it when opening, flips upward when there's more room above, and caps its height to what actually fits — so the list always scrolls inside itself, never off the screen.
Deleting a digit should delete a digit
The typed date field had the strangest bug: backspace didn't work at certain positions. The input mask re-formats on every keystroke and eagerly appends the separator after day and month — so when backspace removed that separator, the mask instantly put it back. The keystroke was being undone by the code meant to help. The shared fix for this elsewhere in the codebase is a client-only directive, which crashed our server-rendered page the moment we borrowed it. The replacement is a small handler that knows whether the current keystroke is a deletion — browsers report this on the event — and simply declines to re-add what the person just removed. Along the way it also fixed a quieter mismatch: the mask inserted slashes while the field's format expected dots, so fully typed dates had never parsed cleanly at all.
Why I'm happy with it
The form now asks only for things a person actually knows: a birthday, a passport, a relationship. Everything that identifies someone arrives from the state, verified; everything that can be refused is refused before it costs money; every refusal speaks the language the person chose. And the two bugs I'll remember — a label forwarding a phantom click and a mask undoing a backspace — were both cases of helpful code helping too hard, which is the most human kind of bug there is.