Build1 distinct publisher3 min readUpdated
A dev.to post makes the case for splitting the customer VAT identifier from an append-only log of validation events, each carrying the consultation number it produced.
The Engineer · Build desk

Compiled by The EngineerSomething wrong?How this is made
A post published on dev.to and republished from vatnode.dev makes a narrow and useful argument: the customer VAT ID design most teams ship, a single mutable `vat_id TEXT` column written with whatever the user typed, works right up until someone asks a dated question [1]. The author's example is the one that lands: an auditor asks what VAT status a customer had on the day you reverse-charged invoice 4471, and the answer lives in one column that has been overwritten twice since [2].
This is a schema problem, not a process problem, because two facts with different lifetimes are being stored in one slot. A customer's VAT ID is a current fact: they re-register, they move entity, they fix a typo, and you want the latest value to be mutable [3]. A validation check is a historical event: it happened at a moment, against a source, and it either produced a consultation number or it did not, and you never want to change it afterwards, because its whole point is recording what was true when you relied on it [4]. Conflate the two and an update to the customer's VAT ID silently invalidates the evidence behind every invoice you have already issued [5]. The fix in the post is one table for the current identifier and latest known status, explicitly allowed to change [6], and a separate append-only table holding the immutable trail of checks [7]. The author frames it as the same split as caching versus storing: Redis holds the fast current answer, PostgreSQL holds the durable evidence [8]. That is a minimum of two storage objects, not one column [9].
The lookup column also has to be normalized before it is useful. EU VAT IDs are case-insensitive alphanumeric strings with a leading two-letter country prefix, so `de 123 456 789`, `DE123456789` and `DE-123-456-789` are the same identifier, and storing them verbatim turns `WHERE vat_id = $1` into a coin flip [10]. Canonical form is uppercase, strip whitespace and punctuation, keep the alphanumerics, applied once before you persist and once before you compare [11]. Keep the raw string as well: it is provenance, and it answers the dispute where a customer says they never entered that number [12]. The country code is then just the first two characters of the normalized value, which removes a separately stored country selection you would otherwise have to keep in sync [13].
Two prefixes are where the post says people break things. Greece is `EL`, not `GR`; the VAT prefix predates the ISO-3166 code and VIES expects `EL`, so rewriting it sends `GR123...` and gets nothing back [14]. Northern Ireland is `XI` after Brexit, a real prefix validatable through VIES rather than a typo for `IE` [15]. Scope is stated as EU-27 plus XI, which is 28 prefixes your normalizer has to leave intact [16], [17].
The post also advises against a fixed length or one universal regex, since length and composition vary by member state; storing a malformed-looking string is fine because it is data, while rejecting a valid ID because your regex was too strict is a bug [18].
What to watch: the published excerpt cuts off mid-`CREATE TABLE customer_vat`, so the column list for the mutable side is not in the version we read [19]. The load-bearing field on the log side is the consultation number, since that is the artefact a check either produced or did not [4]. Before you trust any of this in an audit, check whether your validation client actually returns that number and whether your code persists it on the immutable row rather than discarding it after the boolean.
Follow any of these and your For You feed starts watching them — no settings page required.
Ranked by verification strength, evidence, and original report placement.
The post states that most teams get customer VAT ID storage wrong the same way: they add a single vat_id TEXT column to the customers table, write whatever the user typed, and move on. It was originally published at vatnode.dev, which the author names as the canonical source.
The design works until an auditor asks what VAT status a customer had on the day you reverse-charged invoice 4471, and the answer, stored in one mutable column that has been overwritten twice since, is gone.
The customer-side table (customer_vat, living on or next to the customers table) holds the current identifier and the latest known status, and it is allowed to change.
The recommended split is one table holding the current identifier and another holding the immutable, append-only trail of validation checks.
A customer's VAT ID is a current fact about the customer: it changes when they re-register, move entity, or fix a typo. You want the latest value and you want it to be mutable.
A validation check is a historical event: it happened at a moment, against a source, and it either produced a consultation number or it did not. You never want to change it after the fact, because the point is to record what was true when you relied on it.
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
Single self-published source, internally consistent, no corroboration
Everything in the cluster rests on one dev.to article republished from vatnode.dev. The technical content is specific and self-consistent - runnable normalizer, complete customer_vat DDL, explicit partial unique index - and the prefix claims (EL for Greece, XI for Northern Ireland) are checkable factual assertions. But there is no second publisher, no external documentation cited, no benchmark or production data, and the capture truncates inside the vat_checks definition, so the audit table at the centre of the argument is only partly visible.
No adoption signal in supplied sources
The cluster contains no release, deployment, usage disclosure, benchmark, or third-party account of anyone implementing this schema. It is a prescriptive tutorial, so there is nothing to measure without inferring facts the sources do not provide.
Framing runs slightly ahead of the evidence
The prescriptive content is mundane and well-grounded, but the framing overshoots it modestly: 'most teams get this wrong in the same way' is an unquantified prevalence claim with no survey or data behind it, and the auditor scenario (invoice 4471) is illustrative rather than documented. With zero adoption evidence and one self-interested publisher, the confident universality of the diagnosis is mildly overstated relative to what is shown - hence a small positive gap rather than a large one.
Vendor-adjacent content marketing, partially disclosed
The article is republished from vatnode.dev, which the author names as the canonical source and directs readers to for the latest content, and the schema is written around a validation API whose identifier the DDL calls 'the API's checkId'. That aligns the recommended design with a commercial VAT-validation service. The republication is disclosed at the top, but no explicit commercial-interest statement appears, and dev.to as host applies no independent editorial verification.
Moderate: claims are clear, corroboration and adoption are absent
Confidence is limited by structure rather than by contradiction: one publisher, one article, no adoption measurement, and a truncated capture whose cutoff point the ledger describes incorrectly. What the post says is unambiguous and the design claims are conventional enough to be plausible, which keeps confidence near the middle rather than low.
build
Three services you can delete: queue, cache and search in one Postgres1 distinct publisher
build
A RAG stack lived seven hours before a hosted embedding endpoint returned 4041 distinct publisher
build
94% in the demo, 11% in production: the agent gap is architectural1 distinct publisher
build
The 680 MB database that was really a 17 GB disk: self-hosted support platforms fail at month six1 distinct publisher
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · August 19, 2026