BuilderProof editorial team13 min read47 views

Can One Person Sign Up Twice With the Same Email? A Text-Comparison and Collation Axis (September 2026)

Two strings a human reads as one name can be two different byte sequences, and the default comparison in a generated app calls them different. We pre-register a seven-signal rubric for text-comparison and collation correctness, and name the trap that hides it.

Updated on September 9, 2026

Blueprint line drawing: two horizontal tracks carry near-identical rows of geometric tokens, one a single hexagon and one a hexagon plus a small separate diamond, converging on a circular comparison gate that resolves to one endpoint on one path and two separate endpoints on the other.
Blueprint line drawing: two horizontal tracks carry near-identical rows of geometric tokens, one a single hexagon and one a hexagon plus a small separate diamond, converging on a circular comparison gate that resolves to one endpoint on one path and two separate endpoints on the other.
On this page

Quick answer. In most generated applications, two strings are "the same" only when they are the same sequence of bytes. That rule is invisible while one developer types test data on one keyboard, and it breaks the first time a real user types an accented name on a different operating system, or signs up a second time with a capital letter in their email. This is a proposal for a new BuilderProof axis measuring one narrow, checkable thing: when a generated application decides that two pieces of user-typed text are the same value, does it use a comparison rule that somebody chose, and is that rule applied everywhere the field is read and written. We are pre-registering the rubric before we measure anything. No builder is scored here, and no placement is implied.

Why this needs its own axis

Every application built from a prompt contains a hidden equality operator. It fires when a sign-up form checks whether an address is already registered, when a search box looks for a customer, when a unique constraint decides whether to reject a row. Nobody writes that operator down. It arrives as a default.

The default is byte comparison, and byte comparison is wrong about text in ways that are entirely invisible in a demo. The reason is not exotic. The character n with a tilde can be stored either as one code point or as a plain n followed by a separate combining mark. Both render identically. Both are the same letter to every human who reads them. They are different byte sequences, so a database that compares bytes considers them two different names, and a unique index built on that column will happily store both.

We are naming this pattern the one-keyboard illusion. Every piece of text a developer tests with is typed by that developer, on one machine, with one input method and one locale. That process emits one normalisation form and one case convention, so an application that compares bytes and an application that compares text correctly return identical answers for every string anyone types during development. The evidence available at build time is structurally incapable of separating them.

What the sources actually say

PostgreSQL PostgreSQL states the rule directly, and states that the strict rule is the default. Its collation documentation defines the two behaviours: "A deterministic collation uses deterministic comparisons, which means that it considers strings to be equal only if they consist of the same byte sequence. Nondeterministic comparison may determine strings to be equal even if they consist of different bytes. Typical situations include case-insensitive comparison, accent-insensitive comparison, as well as comparison of strings in different Unicode normal forms."

Then the sentence that decides what a generated schema inherits: "All standard and predefined collations are deterministic, all user-defined collations are deterministic by default." Byte equality is not something a builder chooses badly. It is what arrives when nothing is chosen.

The same project is equally explicit about what a plain unique constraint does. Its citext documentation, describing why that module exists, says of the ordinary approach: "If you declare a column as UNIQUE or PRIMARY KEY, the implicitly generated index is case-sensitive. So it's useless for case-insensitive searches, and it won't enforce uniqueness case-insensitively." And it notes the index consequence of the other common workaround: lowering both sides in the query "won't use an index, unless you create a functional index using lower."

MDN MDN documents the same problem one layer up, in the language most of this code is written in. Its page on string normalisation gives the case directly: the character n with a tilde "can be represented by either of: The single code point U+00F1. The code point for 'n' (U+006E) followed by the code point for the combining tilde (U+0303)", and "since the code points are different, string comparison will not treat them as equal."

We recomputed the published examples rather than quoting them, in Python against Unicode 15.1.0, because a claim you can reproduce is worth more than a claim you can cite. The two forms of that character compare unequal and have lengths 1 and 2; after normalising both to NFC they compare equal with equal lengths. MDN's own worked example, the name Amelie with an acute accent, behaves the same way at lengths 6 and 7. The two byte sequences are the two-byte C3 B1 and the three-byte 6E CC 83, which is why an ordinary unique index stores both.

The same check settles a second claim. MDN notes that locale-specific case mappings "do not follow the default case mappings in Unicode" for some locales, such as Turkish. Lower-casing the Latin capital I with dot above under the default mapping does not produce a plain i. It produces i followed by a combining dot above, exactly as MDN's example shows. Two code points, not one. So the reflex fix, lower-case everything before comparing, does not by itself make these strings equal, and we confirmed that too: lower-casing the two forms of the tilde character leaves them still unequal.

Unicode The Unicode Consortium's Normalization Forms annex explains why the other route works. Normalisation forms are "formally defined normalizations of Unicode strings which make it possible to determine whether any two Unicode strings are equivalent to each other", and once transformed, "A binary comparison of the transformed strings will then determine equivalence." The annex also records that all four transformations "are idempotent", which is what makes normalising on every write safe rather than accumulating drift.

IETF And for the single field this axis cares about most, the relevant standard says something most implementations do not expect. RFC 5321, section 2.4, on email addresses: "The local-part of a mailbox MUST BE treated as case sensitive. Therefore, SMTP implementations MUST take care to preserve the case of mailbox local-parts. In particular, for some hosts, the user 'smith' is different from the user 'Smith'." Two sentences later, the same paragraph: "However, exploiting the case sensitivity of mailbox local-parts impedes interoperability and is discouraged. Mailbox domains follow normal DNS rules and are hence not case sensitive."

One field, two halves, two different comparison rules, and a standard that requires strictness while discouraging reliance on it.

Two tensions the sources state themselves

The first is inside a single PostgreSQL paragraph. Having described nondeterministic collations as giving "a more 'correct' behavior, especially when considering the full power of Unicode and its many special cases", the same passage continues: "Foremost, their use leads to a performance penalty. Note, in particular, that B-tree cannot use deduplication with indexes that use a nondeterministic collation. Also, certain operations are not possible with nondeterministic collations, such as some pattern matching operations."

So the remedy that makes equality mean what a user means is documented, by the people who built it, as removing some pattern matching. Making the uniqueness check correct can break the search feature. These are not independent knobs, and a rubric that pretends otherwise would be scoring a fantasy.

The second is the RFC paragraph above. A project that lower-cases the whole address before storing it is departing from a MUST. A project that compares the whole address byte-exactly is following the MUST and doing the thing the same paragraph calls discouraged and interoperability-impeding. There is no option here that is simply correct, which is precisely why this rubric scores whether a decision was made and applied consistently, and not which decision it was.

The seam between the two documents

Neither source alone tells you how to escape the first tension, and reading them against each other does.

PostgreSQL offers the alternative in a tip attached to that same section: "To deal with text in different Unicode normalization forms, it is also an option to use the functions/expressions normalize and is normalized to preprocess or check the strings, instead of using nondeterministic collations. There are different trade-offs for each approach." It does not say why that works. Unicode's annex does: normalise first, and "A binary comparison of the transformed strings will then determine equivalence."

Put together, the two documents describe an approach neither states on its own. Normalise at the boundary, keep a deterministic collation, and byte comparison becomes correct for the normalisation problem while remaining indexable and fully compatible with pattern matching. It does not solve case or accent folding, which still needs a declared rule. That is the shape a well-built application would have, and it is the shape this rubric is trying to detect.

Proposed rubric

Seven signals, weighted to 100. This is a draft and the weights are the part most open to revision by contributors.

Scroll to see more

SignalWeightWhat a failing case looks like
Identity comparison for account keys24The column that identifies a person carries a unique constraint under the default deterministic collation, so a second sign-up differing only by capitalisation or normalisation form creates a second account and the constraint never fires
Unicode normalisation at the boundary20Text is stored exactly as received, so the same name typed on two operating systems produces two byte sequences, and neither search nor the uniqueness rule can connect them
Case folding is declared, not incidental15Folding happens by whatever lower() does under the collation the database happened to be created with, so the same code gives different answers in two environments and locale-specific mappings are handled by accident
Search comparison matches the stated promise14The interface says matching ignores case or accents, and the query underneath is a byte-exact equality or a pattern operator that the chosen collation cannot serve
The comparison rule is indexable12Case-insensitive matching is implemented by lowering both sides in every query with no expression index behind it, so the feature is correct and degrades permanently as the table grows
Ordering uses a collation someone chose9Lists are sorted under a byte-ordering collation, so accented words sort after unrelated ones and the ordering a reader expects never appears
The rule is written down6Nothing in the emitted project states when two strings are meant to be the same value, so no reviewer can distinguish a deliberate choice from a default nobody noticed

Why the first signal carries the most

Every row on that list produces a bug. Only the first produces a bug the application cannot later repair by itself.

A search that misses a row is annoying and recoverable: fix the query, and the next search finds it. A sort order that offends a reader is cosmetic. But two account rows for one person is a data-integrity outcome that persists after the comparison rule is corrected. The duplicate is already stored. Adding the right constraint afterwards fails, because the table now violates it, and resolving that means merging records that may both own data. The cost of getting this row wrong is paid in a migration, not in a patch, which is what separates it from the rest of the list.

It is also the row most likely to be wrong, because it is the row where the default is silent. Nothing warns you. The insert succeeds.

Four postures

Level 0, Bytes. No declared collation, no normalisation, no folding. Text is compared as received, everywhere. The application behaves correctly for every string the team has ever typed.

Level 1, Folded. Case is handled ad hoc, a lower-casing call in one query and not the next, applied on the read path but not the write path or the constraint. No normalisation anywhere. Behaviour differs between routes that touch the same column.

Level 2, Declared. One comparison rule is chosen deliberately and expressed where the data lives, as a named collation, a case-insensitive type, or normalisation applied at the boundary. The same rule governs the constraint, the search and the ordering for that field.

Level 3, Verified. The declared rule is enforced by a database constraint rather than by application code alone, and the emitted project contains a test that inserts two forms of one value and asserts that one row exists.

Reproduction protocol

  1. Generate an application from a prompt requiring user accounts and a search over a text field, so both an identity column and a searchable column exist.
  2. Export the project. Read the schema. Record the collation on every user-typed text column, and record its absence explicitly rather than by omission.
  3. Establish the attributability baseline: sign up once with plain ASCII, search for it, confirm the ordinary path works. Everything after this is measured against a working application.
  4. Sign up a second time with the same address, changing only the capitalisation of the local part. Record whether a second row is created.
  5. Sign up a third time with the same address, changing only the capitalisation of the domain. Record it separately from step 4, because the standard treats the two halves differently.
  6. Insert a name containing an accented character in composed form. Search for the same name in decomposed form. Record whether the row is found.
  7. Insert the same name in both forms. Record whether the unique constraint fires, or whether both rows persist.
  8. Read what the interface claims. If it states that matching ignores case or accents, test that exact claim rather than a claim you inferred.
  9. Sort a list containing accented and unaccented entries and record the order, comparing it against the order the declared collation predicts.
  10. Run the control that does not depend on us: reproduce the published example from the MDN normalisation page inside the generated application's own runtime, and confirm it behaves as that page documents. An implementation can then be checked against a public constant rather than against our harness.

What this axis is not

It is not concurrent-write safety. That axis asks where an invariant lives, whether "this must not exist twice" is a database constraint or a query issued just before an insert. This one asks what the constraint considers equal. PostgreSQL's own documentation draws the boundary for us: a unique index "won't enforce uniqueness case-insensitively", so the remedy that axis correctly rewards is documented as not seeing this failure at all. Stated both ways: an application can express every uniqueness rule as a real constraint and handle overlapping writes perfectly, and still admit two accounts because a human reads one name where the database reads two byte sequences. And an application can compare text under a flawless accent-insensitive collation and still create duplicates, because it checks with a select before it inserts and two requests interleave. Full detail on that axis is in our concurrent-write safety proposal.

It is not internationalisation. That axis asks whether an application can present itself in the reader's language. This one asks whether it can decide that two strings are one value. Stated both ways: an application translated into twelve locales with correct right-to-left layout can still register one person twice, and a monolingual English application that normalises at the boundary and declares its folding rule handles an accented name correctly with no localisation at all. The i18n axis is defined in our internationalisation output proposal.

It is not per-route title uniqueness. Our metadata axis scores whether every route emits its own non-duplicate title, which is also a question about two strings being the same. The difference is who typed them. Those strings are produced by one generator on one machine, so byte comparison is adequate and normalisation is irrelevant. The strings this axis compares are produced by two people on two keyboards, which is exactly the condition under which byte comparison stops being adequate. Stated both ways: an application can emit perfectly distinct titles on every route and still merge nothing correctly in its user table, and an application with impeccable text comparison can ship the same placeholder title on forty routes. That axis is our generated-app SEO and metadata proposal.

For completeness, this is also not the numeric question. A separate axis already covers when two numbers are equal, which is a different failure with a different remedy.

Open questions

We are stating these rather than quietly deciding them.

Should the email signal reward following the RFC or contradicting it? The standard requires case-sensitive treatment of the local part and discourages depending on that. Our current position is that the rubric should score consistency and disclosure, not the direction of the choice, but a contributor could reasonably argue that any application which lets one person hold two accounts has failed regardless of what the standard permits.

Is normalisation at the boundary or a nondeterministic collation the better target? Both are documented, both work, and the sources say only that there are "different trade-offs for each approach." We do not currently believe a benchmark should prefer one, but the rubric as drafted is slightly easier to score for the collation route, and that is a bias worth naming.

Does the stability caveat matter in practice? Unicode's stability policy guarantees that normalisation results do not change across versions, "as long as the string contains only assigned characters according to both versions", and notes that a string containing characters unassigned in the implementing version "might not be in normalized form according to a future version of Unicode." Whether that edge is worth a rubric row or is noise at application scale is genuinely unresolved.

References

All fetched and read on September 9, 2026.

This page is an axis proposal open for community edits, not a leaderboard. It scores no builder and implies no placement. Our scoring conventions are described in our published methodology.

Cite this benchmark

Plain text
BuilderProof editorial team. "Can One Person Sign Up Twice With the Same Email? A Text-Comparison and Collation Axis (September 2026)". BuilderProof, September 2026. https://www.builderproof.org/benchmarks/can-one-person-sign-up-twice-text-comparison-axis-september-2026.
BibTeX
@misc{builderproof-can-one-person-sign-up-twice-text-comparison-axis-september-2026,
  title  = {{Can One Person Sign Up Twice With the Same Email? A Text-Comparison and Collation Axis (September 2026)}},
  author = {{BuilderProof editorial team}},
  year   = {2026},
  month  = {sep},
  howpublished = {\url{https://www.builderproof.org/benchmarks/can-one-person-sign-up-twice-text-comparison-axis-september-2026}},
  note   = {BuilderProof, builderproof.org}
}

Frequently asked questions

Why does a missing text-comparison rule stay invisible during development?

Because every string a developer tests with is typed by that developer, on one machine, with one input method and one locale. That process emits one Unicode normalisation form and one case convention, so an application comparing raw bytes and an application comparing text correctly return the same answer for every input anyone tries. We call this the one-keyboard illusion. The evidence available at build time cannot separate the working case from the broken one.

Is byte comparison actually the default in a generated application?

In PostgreSQL it is, and PostgreSQL sits under most of what these builders emit. Its documentation states that a deterministic collation considers strings equal only if they consist of the same byte sequence, and that all standard and predefined collations are deterministic while all user-defined collations are deterministic by default. Byte equality is not a bad choice a builder makes. It is what arrives when nothing is chosen.

Does lower-casing everything before comparing solve this?

No, and that is measurable rather than a matter of opinion. Lower-casing does nothing about Unicode normalisation: the two representations of an accented character remain different byte sequences after both are lower-cased. Lower-casing also has locale-specific behaviour that MDN documents as departing from the Unicode default in some locales, and PostgreSQL notes that lowering both sides in a query will not use an index unless a functional index is created for it.

Why does this axis not simply require a case-insensitive email comparison?

Because RFC 5321 says the local part of a mailbox must be treated as case sensitive, and in the same paragraph says that exploiting that case sensitivity impedes interoperability and is discouraged. Neither lower-casing the whole address nor comparing it byte-exactly is unambiguously correct. The rubric therefore scores whether a decision was made, applied everywhere the field is read and written, and written down, rather than scoring which decision it was.

Is this the same as the concurrent-write safety axis?

No. That axis asks where an invariant lives, whether uniqueness is a database constraint or a query issued just before an insert. This axis asks what the constraint considers equal. PostgreSQL's own citext documentation draws the line: an implicitly generated unique index is case-sensitive and will not enforce uniqueness case-insensitively, so the remedy the other axis correctly rewards is documented as unable to see this failure.

Methodology

Internationalization output: a proposed benchmark axis for AI app builders (August 2026)

As of August 2026, none of the five leading AI app builders (v0, Lovable, Replit, Base44, and Bolt.new) documents built-in internationalization (i18n) scaffolding. This axis proposes a neutral, reproducible way to score i18n output across five sub-criteria and finds multilingual support is a category-wide gap that teams currently fill with standard libraries or third-party tools.

9 min read136
Methodology

Generated-App SEO and Meta Output Quality: A Proposed Axis for What AI App Builders Actually Emit to Crawlers (August 2026)

A candidate BuilderProof benchmark axis that scores the crawler-facing artifacts AI app builders emit by default: per-route titles, canonicals, social cards, robots.txt, sitemap.xml, structured data, and whether route content reaches a crawler at all. Rubric, four rendering postures found in the vendor docs, a dual user-agent reproduction protocol, and an open call for comment.

17 min read102