A card gets declined in under a second. A pincode service tells you the city does not match while the user is still looking at the field. You write a rule, send a bad value, read the error, fix the rule.
We do not have any of that.
CertiSetu files applications for four Uttar Pradesh government certificates: domicile, income, caste and EWS. A user verifies once through DigiLocker, uploads their documents, and the application goes to the state's e-District system. From a complete filing, the first three take 7 to 10 working days. EWS takes 15 to 20.
If the department rejects the file, that is when we find out. And the rejection usually does not say which field caused it.
So the only authoritative test we have takes two weeks and comes back with roughly one bit of information.
What that rules out
The normal loop is to ship a validator, watch it fail in production, then tighten it. That works because production answers quickly and specifically.
Here production answers slowly and vaguely, and every iteration costs a real person a fortnight and usually a deadline. You cannot learn by submitting.
Anything we are able to decide ourselves has to be decided before the file goes out, because afterwards we are blind.
Three kinds of check
Sorting the checks turned out to be more useful than writing any one of them.
Some things are true or false on their own. A date parses. A file opens. An income figure is a number and not a string with a rupee sign in it. These are cheap, they belong in the browser, and they are almost never why something gets rejected.
Some things are only true or false in relation to something else the user gave us. The name on the Aadhaar has to match the name on the ration card. The address on the electricity bill has to be the address typed into the form. An EWS application needs an income certificate from the right financial year, not just any income certificate. This is where the rejections actually come from.
And some things only the department can decide, like whether a particular tehsil is accepting a given affidavit format this month. We cannot check those at all. We can only write down what happened.
The middle group is the whole problem, and it does not fit the shape most validation libraries are built around.
Field validators are the wrong shape
A field validator takes one value and returns a verdict:
const validators = {
annualIncome: v => Number.isInteger(v) && v > 0,
applicantName: v => v.trim().length > 1,
};
The checks that matter need the whole application, and they need to name the two things that disagree, because a person is going to have to open both. Something closer to:
{
id: "name-match-aadhaar-ration",
applies: app => app.docs.aadhaar && app.docs.rationCard,
check: app => norm(app.docs.aadhaar.name) === norm(app.docs.rationCard.name),
evidence: ["aadhaar", "rationCard"],
message: "This name is spelled differently on these two documents.",
}
That evidence list is the part worth keeping. "Validation failed" tells whoever has to fix it nothing. "These two files disagree, here they are" is a job someone can finish in a minute.
A slow oracle needs a fast stand-in
Since we cannot ask the department anything, we put a person in front of it. Every application is reviewed by a verified agent against the department's current rules before it is filed.
That is a bottleneck. It is also the only fast feedback in the system. A reviewer catches a mismatched name in a minute, where the department catches it in ten working days and does not say what it was.
The reviewer is also where new rules come from. When something is rejected, the reason gets worked out once and then becomes a check that runs on every application after it. A rejection that does not turn into a rule costs two weeks and teaches nothing.
The inputs are photographs
This shaped the code more than I expected. People fill these forms on a phone, at night, with the document lying flat on a bed. The image is rotated. The flash has blown out one corner. A ward councillor's stamp is legible to a human and not to much else.
Anything that reads these automatically has to treat "I could not tell" as an ordinary outcome rather than an error. We would rather send an unclear document to a person than let a confident parser read the wrong year off a smudged stamp and drop the file into a two-week hole.
Where this leaves us
You cannot iterate against the system of record when the system of record answers in working days. So you iterate against your own record of what it rejected, and you spend the engineering effort on the step before submission instead of the step after it.
None of this is a hard distributed systems problem. It is one slow, vague, unmockable dependency, and nearly every design decision here falls out of that single fact.
CertiSetu is an independent private platform, not a government portal. Certificates are issued and approved by the relevant government authorities, and anyone can file directly on the UP e-District portal. Our guides go through the document requirements for each certificate, and there is more on the form design side of this at certisetu.hashnode.dev.
Top comments (0)