Appearance
Contracts and signing
A merchant agreement has to answer one question, years later, to someone who was not there:
What did this merchant agree to?
That question is harder than it sounds, and the honest answer requires three separate things. Verity builds all three, and the reason is worth understanding before the mechanics.
The problem Nafath cannot solve
Saudi Arabia signs through Nafath, and that is not a preference — SAMA requires it. Nafath is strong: the person authenticates in a government app and approves a request, which is far better evidence than a typed name in a box.
But look at what its token actually says. These are the complete claims:
json
{
"aud": "SP_NAME",
"iss": "Nafath App",
"transId": "3a4a3b26-3b8f-4d4f-90fe-d1a4ef834e8d",
"status": "COMPLETED",
"iat": 1661155614,
"nbf": 1661155914,
"exp": 1661155914
}There is nothing about a document. Nafath attests that a person approved a transaction. Nothing about the contract is sent to it, so nothing about the contract comes back.
That is not a flaw in Nafath — it is an identity service, not a notary. But it means a signature obtained through Nafath, on its own, proves that somebody said yes to something. It does not say what.
This gap cannot be closed by changing provider
Because SAMA mandates Nafath, "use a provider that signs the document" is not an available move. The binding has to be recorded on our side, or it does not exist.
What the legacy system could not answer
The system Verity replaces had the same provider and did not record the binding. Three things compounded:
- The contract pointed at a mutable template, so the wording changed whenever anyone fixed a typo
- No copy of the rendered document was kept
signed_datefell back toTime.nowwhen no Nafath attempt had completed
The result: for contracts that had already been signed, "what did this merchant agree to?" had no answer. Not a hard answer — no answer. The wording had moved, nothing had been archived, and the timestamp might have been the moment somebody opened a page.
The three things that answer it
| What | Why alone it is not enough |
|---|---|
| An immutable template version | Captures the wording, not the merchant's own details or fees |
| A SHA-256 digest of the exact bytes | Proves nothing a person can read |
| The archived bytes themselves | Can be swapped, if nothing pins them |
Together they answer the question. A contract points at wording that cannot change, carries a digest of the exact document rendered from it, and keeps those bytes. Any two of the three leave a hole.
See Templates for the first and Signing for how an authorization is bound to the other two.
Two claims, kept apart
Once a signature arrives, Verity records two statements and never merges them:
The provider attests that this person, authenticated in Nafath, approved a transaction.
Verity attests that this document — these exact bytes, this digest — was what was on screen when they were asked.
Merging them would produce a sentence neither party said: "Nafath confirms this merchant signed this contract." Nafath confirms no such thing, and a compliance officer who relies on that sentence has been misled by us rather than by the provider.
A module, not a Verity feature
Signing knows a document and a signatory, and nothing else. It does not know what a merchant is, what a country requires, or what a check is.
That is enforced rather than intended: an invariant walks the module's files and fails if one names anything from Verity's domain. Verity may know about signing; signing may not know about Verity.
| The module holds | The document, its versions, the values it declares, the signatory, the binding, the attempts |
| Verity supplies | Values, a signatory, and which provider to ask |
| The bridge | Entities::MerchantAgreement — the one place that knows both |
An envelope's link back to whatever it is about is polymorphic and optional, so a caller can attach anything and the module never reads it.
What each country needs
| Saudi Arabia | Bahrain | |
|---|---|---|
| Identity | Nafath | eKey (iGA, bound to CPR) |
| Signature | Nafath | BENEFIT / OneSign (TRA-licensed) |
| Built | Yes | No |
| Configured | No | — |
Nafath's single role splits in two in Bahrain, which is why identity and signature providers are separate fields rather than one assumed to cover both.
Two ways to sign, and they are not equal
| By link | With Nafath | |
|---|---|---|
| Binds the document | Yes | Yes |
| Establishes who signed | No | Yes |
| Works today | Yes | Needs credentials and a Saudi-hosted callback |
Both record the binding the same way. They differ in one thing, and it is the important one: a click-through proves that somebody holding a link agreed to these exact bytes, and nothing more.
Envelope#assurance_statement is a single sentence composed by the model, so no screen can print "Signed" without printing what that word is worth. An invariant walks every assurance value and fails if one has no statement, if two read alike, or if a non-provider assurance claims an identity check.
Where the person signs
With Nafath, in the Nafath app — Verity serves no signature screen there and does not need to.
But Nafath shows the person a service type and a number, never the document, so the reading step is ours either way. The merchant has to see the contract before the request is made, or the consent Verity records describes something that did not happen. See where the person actually is.
Status
Verity can prepare a contract, archive it, put it in front of a signatory, and record a click-through acceptance bound to the exact bytes shown.
It cannot yet obtain a Nafath signature: the settings are unset, and Nafath cannot be exercised without a staging deployment whose callback Elm can reach. See what is not built yet.