From 33405b8ae9c31e344d731c2625fbcac24ce22901 Mon Sep 17 00:00:00 2001 From: Henning Perl Date: Tue, 14 Apr 2026 14:05:35 +0200 Subject: [PATCH 1/7] docs(kratos): add phone number normalization migration guide Document the new kratos migrate normalize-phone-numbers command for self-hosted (OSS and OEL) Kratos administrators. The guide explains why normalization matters, the required deploy-then-migrate rollout sequence, command flags, and how to interpret the output. Also link to the new guide from the upgrade guide so admins see it when planning a version upgrade that introduces phone normalization. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../kratos/guides/normalize-phone-numbers.mdx | 141 ++++++++++++++++++ docs/kratos/guides/upgrade.mdx | 5 + src/sidebar.ts | 1 + 3 files changed, 147 insertions(+) create mode 100644 docs/kratos/guides/normalize-phone-numbers.mdx diff --git a/docs/kratos/guides/normalize-phone-numbers.mdx b/docs/kratos/guides/normalize-phone-numbers.mdx new file mode 100644 index 0000000000..0642381fd4 --- /dev/null +++ b/docs/kratos/guides/normalize-phone-numbers.mdx @@ -0,0 +1,141 @@ +--- +id: normalize-phone-numbers +title: Normalize phone numbers to E.164 +sidebar_label: Normalize phone numbers +--- + +Starting with this release, Ory Kratos normalizes phone numbers to +[E.164 format](https://en.wikipedia.org/wiki/E.164) when they're used as +identifiers, verifiable addresses, or recovery addresses. New data is normalized +on write. Existing data continues to work through a backward-compatible lookup, +but you should run the `normalize-phone-numbers` migration command after +upgrading to converge all rows to E.164. + +This guide is for self-hosted Kratos administrators (OSS and OEL). Ory Network +customers don't need to take any action. + +:::info + +Back up your database before running the migration. + +::: + +## Why normalize + +Before this change, Kratos stored phone numbers exactly as users entered them. +A user who registered with `+49 176 671 11 638` and another who registered with +`+4917667111638` would create two separate identities for the same phone number. +Lookups, recovery, and verification could behave inconsistently depending on the +input format. + +After normalization, all phone numbers are stored in E.164 format +(for example, `+4917667111638`). Lookups match regardless of how the user +formatted the input. + +## Rollout sequence + +Run the steps in this exact order: + +1. **Deploy the new Kratos version.** + The new code normalizes phone numbers on write and uses a backward-compatible + lookup that matches both E.164 and legacy formats. Existing users can still + log in with whatever format they originally registered with. + +2. **Run the migration command.** + After the deploy completes and traffic is stable, run: + + ``` + kratos migrate normalize-phone-numbers + ``` + + Or with the DSN from the environment: + + ``` + export DSN=... + kratos migrate normalize-phone-numbers -e + ``` + + The command iterates over `identity_credential_identifiers`, + `identity_verifiable_addresses`, and `identity_recovery_addresses` and + rewrites any non-E.164 phone numbers in place. + +:::caution + +Don't run the migration before deploying the new Kratos version. The previous +version does exact-string matching on identifiers. If you normalize the database +first, users who type their phone number in the original (non-E.164) format +won't be able to log in until the new code is deployed. + +::: + +## What the command does + +The command uses keyset pagination to scan three tables in batches: + +| Table | Column | Filter | +|-------|--------|--------| +| `identity_credential_identifiers` | `identifier` | `identifier LIKE '+%'` | +| `identity_verifiable_addresses` | `value` | `via = 'sms'` | +| `identity_recovery_addresses` | `value` | `via = 'sms'` | + +For each row, the command parses the value with the +[`nyaruka/phonenumbers`](https://github.com/nyaruka/phonenumbers) library and +rewrites it to E.164 if parsing succeeds. Rows that fail to parse (for example, +an OIDC subject that happens to start with `+`) are left untouched and counted +as skipped. + +The command is **idempotent**: running it twice is safe. The second run only +reports skipped rows. + +## Flags + +| Flag | Default | Description | +|------|---------|-------------| +| `-e`, `--read-from-env` | `false` | Read the database connection string from the `DSN` environment variable. | +| `-b`, `--batch-size` | `1000` | Number of rows to process per batch. | +| `--dry-run` | `false` | Report what would change without writing. | + +Use `--dry-run` first to preview the changes: + +``` +kratos migrate normalize-phone-numbers --dry-run -e +``` + +Each row that would be updated is printed in the form: + +``` +[dry-run] identity_credential_identifiers : "+49 176 671 11 638" -> "+4917667111638" +``` + +## Output + +After processing all three tables, the command prints a summary: + +``` +=== Summary === +identity_credential_identifiers: scanned=1234 updated=42 skipped=1192 errors=0 +identity_verifiable_addresses: scanned=987 updated=15 skipped=972 errors=0 +identity_recovery_addresses: scanned=987 updated=15 skipped=972 errors=0 +``` + +- `scanned`: rows examined. +- `updated`: rows rewritten to E.164 (or rows that *would* be rewritten in dry-run mode). +- `skipped`: rows already in E.164 format, or values that aren't valid phone numbers. +- `errors`: rows that failed to update. Errors are logged to stderr with the row ID and source value. + +## Duplicate handling + +If the migration finds two rows that normalize to the same E.164 value (for +example, `+49 176 671 11 638` and `+4917667111638` for the same user), the +update fails on the second row with a unique constraint violation, which the +command logs as an error and skips. You can resolve the duplicate manually and +re-run the command. + +In practice, duplicates are rare. Most identities have only one phone identifier +per credential type. + +## Rolling back + +The migration only converts non-E.164 values to E.164. It doesn't store the +original value, so there's no automatic rollback. If you need to revert, restore +from the backup you took before running the command. diff --git a/docs/kratos/guides/upgrade.mdx b/docs/kratos/guides/upgrade.mdx index d8171bea6a..eb4b00d038 100644 --- a/docs/kratos/guides/upgrade.mdx +++ b/docs/kratos/guides/upgrade.mdx @@ -18,6 +18,11 @@ Back up your data! Applying upgrades can lead to data loss if handled incorrectl 1. Update the [Ory Kratos SDK](../sdk/01_overview.md) if used in your application. 1. [Install](../install.mdx) the new version of Ory Kratos. 1. Run [`kratos migrate sql`](../cli/kratos-migrate-sql.md) to run the SQL migrations to the new database schema. +1. If you are upgrading to a version that introduces phone number normalization, run + [`kratos migrate normalize-phone-numbers`](./normalize-phone-numbers.mdx) after the new + version is deployed and serving traffic. This rewrites existing phone identifiers to + E.164 format. See the [phone normalization guide](./normalize-phone-numbers.mdx) for + the recommended rollout sequence. Should you run into problems with the upgrade, consider a stepped upgrade and please visit the community [chat](https://slack.ory.com/) or start a [discussion](https://github.com/ory/kratos/discussions). diff --git a/src/sidebar.ts b/src/sidebar.ts index 73aec49ab1..a54af94256 100644 --- a/src/sidebar.ts +++ b/src/sidebar.ts @@ -607,6 +607,7 @@ const kratos: SidebarItemsConfig = [ "kratos/guides/docker", "kratos/guides/deploy-kratos-example", "kratos/guides/upgrade", + "kratos/guides/normalize-phone-numbers", "kratos/guides/production", "kratos/guides/multi-tenancy-multitenant", "self-hosted/operations/scalability", From c01ba19b739a7771a23147d07e7ce7e7f74ad8bf Mon Sep 17 00:00:00 2001 From: Henning Perl Date: Tue, 14 Apr 2026 18:01:47 +0200 Subject: [PATCH 2/7] chore: format --- .../kratos/guides/normalize-phone-numbers.mdx | 141 ------------------ docs/kratos/guides/upgrade.mdx | 15 +- 2 files changed, 6 insertions(+), 150 deletions(-) delete mode 100644 docs/kratos/guides/normalize-phone-numbers.mdx diff --git a/docs/kratos/guides/normalize-phone-numbers.mdx b/docs/kratos/guides/normalize-phone-numbers.mdx deleted file mode 100644 index 0642381fd4..0000000000 --- a/docs/kratos/guides/normalize-phone-numbers.mdx +++ /dev/null @@ -1,141 +0,0 @@ ---- -id: normalize-phone-numbers -title: Normalize phone numbers to E.164 -sidebar_label: Normalize phone numbers ---- - -Starting with this release, Ory Kratos normalizes phone numbers to -[E.164 format](https://en.wikipedia.org/wiki/E.164) when they're used as -identifiers, verifiable addresses, or recovery addresses. New data is normalized -on write. Existing data continues to work through a backward-compatible lookup, -but you should run the `normalize-phone-numbers` migration command after -upgrading to converge all rows to E.164. - -This guide is for self-hosted Kratos administrators (OSS and OEL). Ory Network -customers don't need to take any action. - -:::info - -Back up your database before running the migration. - -::: - -## Why normalize - -Before this change, Kratos stored phone numbers exactly as users entered them. -A user who registered with `+49 176 671 11 638` and another who registered with -`+4917667111638` would create two separate identities for the same phone number. -Lookups, recovery, and verification could behave inconsistently depending on the -input format. - -After normalization, all phone numbers are stored in E.164 format -(for example, `+4917667111638`). Lookups match regardless of how the user -formatted the input. - -## Rollout sequence - -Run the steps in this exact order: - -1. **Deploy the new Kratos version.** - The new code normalizes phone numbers on write and uses a backward-compatible - lookup that matches both E.164 and legacy formats. Existing users can still - log in with whatever format they originally registered with. - -2. **Run the migration command.** - After the deploy completes and traffic is stable, run: - - ``` - kratos migrate normalize-phone-numbers - ``` - - Or with the DSN from the environment: - - ``` - export DSN=... - kratos migrate normalize-phone-numbers -e - ``` - - The command iterates over `identity_credential_identifiers`, - `identity_verifiable_addresses`, and `identity_recovery_addresses` and - rewrites any non-E.164 phone numbers in place. - -:::caution - -Don't run the migration before deploying the new Kratos version. The previous -version does exact-string matching on identifiers. If you normalize the database -first, users who type their phone number in the original (non-E.164) format -won't be able to log in until the new code is deployed. - -::: - -## What the command does - -The command uses keyset pagination to scan three tables in batches: - -| Table | Column | Filter | -|-------|--------|--------| -| `identity_credential_identifiers` | `identifier` | `identifier LIKE '+%'` | -| `identity_verifiable_addresses` | `value` | `via = 'sms'` | -| `identity_recovery_addresses` | `value` | `via = 'sms'` | - -For each row, the command parses the value with the -[`nyaruka/phonenumbers`](https://github.com/nyaruka/phonenumbers) library and -rewrites it to E.164 if parsing succeeds. Rows that fail to parse (for example, -an OIDC subject that happens to start with `+`) are left untouched and counted -as skipped. - -The command is **idempotent**: running it twice is safe. The second run only -reports skipped rows. - -## Flags - -| Flag | Default | Description | -|------|---------|-------------| -| `-e`, `--read-from-env` | `false` | Read the database connection string from the `DSN` environment variable. | -| `-b`, `--batch-size` | `1000` | Number of rows to process per batch. | -| `--dry-run` | `false` | Report what would change without writing. | - -Use `--dry-run` first to preview the changes: - -``` -kratos migrate normalize-phone-numbers --dry-run -e -``` - -Each row that would be updated is printed in the form: - -``` -[dry-run] identity_credential_identifiers : "+49 176 671 11 638" -> "+4917667111638" -``` - -## Output - -After processing all three tables, the command prints a summary: - -``` -=== Summary === -identity_credential_identifiers: scanned=1234 updated=42 skipped=1192 errors=0 -identity_verifiable_addresses: scanned=987 updated=15 skipped=972 errors=0 -identity_recovery_addresses: scanned=987 updated=15 skipped=972 errors=0 -``` - -- `scanned`: rows examined. -- `updated`: rows rewritten to E.164 (or rows that *would* be rewritten in dry-run mode). -- `skipped`: rows already in E.164 format, or values that aren't valid phone numbers. -- `errors`: rows that failed to update. Errors are logged to stderr with the row ID and source value. - -## Duplicate handling - -If the migration finds two rows that normalize to the same E.164 value (for -example, `+49 176 671 11 638` and `+4917667111638` for the same user), the -update fails on the second row with a unique constraint violation, which the -command logs as an error and skips. You can resolve the duplicate manually and -re-run the command. - -In practice, duplicates are rare. Most identities have only one phone identifier -per credential type. - -## Rolling back - -The migration only converts non-E.164 values to E.164. It doesn't store the -original value, so there's no automatic rollback. If you need to revert, restore -from the backup you took before running the command. diff --git a/docs/kratos/guides/upgrade.mdx b/docs/kratos/guides/upgrade.mdx index eb4b00d038..10ce345285 100644 --- a/docs/kratos/guides/upgrade.mdx +++ b/docs/kratos/guides/upgrade.mdx @@ -11,18 +11,15 @@ Back up your data! Applying upgrades can lead to data loss if handled incorrectl ::: -1. Review breaking changes. - Visit the [CHANGELOG.md](https://github.com/ory/kratos/blob/master/CHANGELOG.md) to see if breaking changes have been - introduced in the version you are upgrading to. +1. Review breaking changes. Visit the [CHANGELOG.md](https://github.com/ory/kratos/blob/master/CHANGELOG.md) to see if breaking + changes have been introduced in the version you are upgrading to. 1. Backup your data. 1. Update the [Ory Kratos SDK](../sdk/01_overview.md) if used in your application. 1. [Install](../install.mdx) the new version of Ory Kratos. 1. Run [`kratos migrate sql`](../cli/kratos-migrate-sql.md) to run the SQL migrations to the new database schema. -1. If you are upgrading to a version that introduces phone number normalization, run - [`kratos migrate normalize-phone-numbers`](./normalize-phone-numbers.mdx) after the new - version is deployed and serving traffic. This rewrites existing phone identifiers to - E.164 format. See the [phone normalization guide](./normalize-phone-numbers.mdx) for - the recommended rollout sequence. +1. If you are upgrading to a version that introduces phone number normalization, run kratos migrate normalize-phone-numbers` after + the new version is deployed and serving traffic. This rewrites existing phone identifiers to E.164 format. See the + [phone normalization guide](./normalize-phone-numbers.mdx) for the recommended rollout sequence. Should you run into problems with the upgrade, consider a stepped upgrade and please visit the community [chat](https://slack.ory.com/) or start a [discussion](https://github.com/ory/kratos/discussions). @@ -31,5 +28,5 @@ Should you run into problems with the upgrade, consider a stepped upgrade and pl In [Ory Kratos v0.7](https://github.com/ory/kratos/blob/v0.7.0-alpha.1/CHANGELOG.md#breaking-changes) the cookie behavior has changed. Review -[changes in the exemplary self-service user interface](https://github.com/ory/kratos-selfservice-ui-node/commit/e7fa292968111e06401fcfc9b1dd0e8e285a4d87). +[changes in the exemplary self-service user interface](https://github.com/ory/kratos-selfservice-ui-node/commit/e7fa292968111e06401fcfc9b1dd0e8e285a4d87). Visit the [Cookie Settings](https://www.ory.com/kratos/docs/guides/multi-domain-cookies/#cookies) document for more information. From 0b993f16101977139177ca9aec085a46c20e9390 Mon Sep 17 00:00:00 2001 From: Henning Perl Date: Tue, 14 Apr 2026 18:59:33 +0200 Subject: [PATCH 3/7] fix build --- package-lock.json | 53 +++++++++++++++++++++++++++++++++++++++++++++-- src/sidebar.ts | 1 - 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index e1720c1264..1ba744161c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -271,6 +271,7 @@ "version": "5.44.0", "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.44.0.tgz", "integrity": "sha512-/FRKUM1G4xn3vV8+9xH1WJ9XknU8rkBGlefruq9jDhYUAvYozKimhrmC2pRqw/RyHhPivmgZCRuC8jHP8piz4Q==", + "peer": true, "dependencies": { "@algolia/client-common": "5.44.0", "@algolia/requester-browser-xhr": "5.44.0", @@ -447,6 +448,7 @@ "node_modules/@babel/core": { "version": "7.26.0", "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.0", @@ -2433,6 +2435,7 @@ "url": "https://opencollective.com/csstools" } ], + "peer": true, "engines": { "node": ">=18" }, @@ -2454,6 +2457,7 @@ "url": "https://opencollective.com/csstools" } ], + "peer": true, "engines": { "node": ">=18" } @@ -2558,6 +2562,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -2963,6 +2968,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -3901,6 +3907,7 @@ "version": "3.9.2", "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.9.2.tgz", "integrity": "sha512-C5wZsGuKTY8jEYsqdxhhFOe1ZDjH0uIYJ9T/jebHwkyxqnr4wW0jTkB72OMqNjsoQRcb0JN3PcSeTwFlVgzCZg==", + "peer": true, "dependencies": { "@docusaurus/core": "3.9.2", "@docusaurus/logger": "3.9.2", @@ -4185,6 +4192,7 @@ "version": "3.9.2", "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.9.2.tgz", "integrity": "sha512-6c4DAbR6n6nPbnZhY2V3tzpnKnGL+6aOsLvFL26VRqhlczli9eWG0VDUNoCQEPnGwDMhPS42UhSAnz5pThm5Ag==", + "peer": true, "dependencies": { "@docusaurus/mdx-loader": "3.9.2", "@docusaurus/module-type-aliases": "3.9.2", @@ -4288,6 +4296,7 @@ "version": "3.9.2", "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.9.2.tgz", "integrity": "sha512-lBSBiRruFurFKXr5Hbsl2thmGweAPmddhF3jb99U4EMDA5L+e5Y1rAkOS07Nvrup7HUMBDrCV45meaxZnt28nQ==", + "peer": true, "dependencies": { "@docusaurus/logger": "3.9.2", "@docusaurus/types": "3.9.2", @@ -5470,6 +5479,7 @@ "node_modules/@mdx-js/react": { "version": "3.1.0", "license": "MIT", + "peer": true, "dependencies": { "@types/mdx": "^2.0.0" }, @@ -5529,6 +5539,7 @@ "node_modules/@octokit/core": { "version": "5.2.0", "license": "MIT", + "peer": true, "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", @@ -5877,6 +5888,7 @@ "node_modules/@rjsf/core": { "version": "5.24.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "lodash": "^4.17.21", "lodash-es": "^4.17.21", @@ -5897,6 +5909,7 @@ "resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-5.24.1.tgz", "integrity": "sha512-A25fFj/TNz5bKikCIs20DiedKAalLuAQ7vUX9VQkD2hps5C9YVr0dJgSlsPa5kzl6lQMaRsNouTx8E1ZdLV2fg==", "license": "Apache-2.0", + "peer": true, "dependencies": { "json-schema-merge-allof": "^0.8.1", "jsonpointer": "^5.0.1", @@ -6186,6 +6199,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", + "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -6285,6 +6299,7 @@ "dev": true, "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@swc/counter": "^0.1.2", "@swc/types": "^0.1.5" @@ -7261,7 +7276,8 @@ }, "node_modules/@types/json-schema": { "version": "7.0.15", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/keyv": { "version": "3.1.4", @@ -7293,6 +7309,7 @@ "node_modules/@types/node": { "version": "22.8.4", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.19.8" } @@ -7335,6 +7352,7 @@ "node_modules/@types/react": { "version": "18.3.12", "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -7657,6 +7675,7 @@ "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -7737,6 +7756,7 @@ "node_modules/ajv": { "version": "8.17.1", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -7778,6 +7798,7 @@ "version": "5.44.0", "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.44.0.tgz", "integrity": "sha512-f8IpsbdQjzTjr/4mJ/jv5UplrtyMnnciGax6/B0OnLCs2/GJTK13O4Y7Ff1AvJVAaztanH+m5nzPoUq6EAy+aA==", + "peer": true, "dependencies": { "@algolia/abtesting": "1.10.0", "@algolia/client-abtesting": "5.44.0", @@ -8070,6 +8091,7 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "peer": true, "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", @@ -8566,6 +8588,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -8929,6 +8952,7 @@ "version": "11.0.3", "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", + "peer": true, "dependencies": { "@chevrotain/cst-dts-gen": "11.0.3", "@chevrotain/gast": "11.0.3", @@ -9570,6 +9594,7 @@ "version": "3.38.1", "hasInstallScript": true, "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/core-js" @@ -9859,6 +9884,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -10169,6 +10195,7 @@ "node_modules/cytoscape": { "version": "3.31.0", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10" } @@ -10534,6 +10561,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "peer": true, "engines": { "node": ">=12" } @@ -12070,6 +12098,7 @@ "node_modules/file-loader/node_modules/ajv": { "version": "6.12.6", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -14731,6 +14760,7 @@ "node_modules/jsep": { "version": "1.4.0", "license": "MIT", + "peer": true, "engines": { "node": ">= 10.16.0" } @@ -17892,6 +17922,7 @@ "resolved": "https://registry.npmjs.org/mobx/-/mobx-6.13.7.tgz", "integrity": "sha512-aChaVU/DO5aRPmk1GX8L+whocagUUpBQqoPtJk+cm7UOXUk87J4PeWCh6nNmTTIfEhiR9DI/+FnA8dln/hTK7g==", "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/mobx" @@ -18372,6 +18403,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -19352,6 +19384,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.1", @@ -20207,6 +20240,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -20775,6 +20809,7 @@ "version": "3.2.5", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -21166,6 +21201,7 @@ "version": "6.12.6", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -21232,6 +21268,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.0.1.tgz", "integrity": "sha512-nVRaZCuEyvu69sWrkdwjP6QY57C+lY+uMNNMyWUFJb9Z/JlaBOQus7mSMfGYsblv7R691u6SSJA/dX9IRnyyLQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -21241,6 +21278,7 @@ "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.6.8.tgz", "integrity": "sha512-yD6uN78XlFOkETQp6GRuVe0s5509x3XYx8PfPbirwFTYCj5/RfmSs9YZGCwkUrhZNFzj7tZPdpb+3k50mK1E4g==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.14.0", "@restart/context": "^2.1.4", @@ -21270,6 +21308,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.1.tgz", "integrity": "sha512-3TJg51HSbJiLVYCS6vWwWsyqoS36aGEOCmtLLHxROlSZZ5Bk10xpxHFbrCu4DdqgR85DDc9Vucxqhai3g2xjtA==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.25.0" }, @@ -21322,6 +21361,7 @@ "name": "@docusaurus/react-loadable", "version": "6.0.0", "license": "MIT", + "peer": true, "dependencies": { "@types/react": "*" }, @@ -21366,6 +21406,7 @@ "node_modules/react-router": { "version": "5.3.4", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.12.13", "history": "^4.9.0", @@ -23816,6 +23857,7 @@ "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.18.tgz", "integrity": "sha512-Mvf3gJFzZCkhjY2Y/Fx9z1m3dxbza0uI9H1CbNZm/jSHCojzJhQ0R7bByrlFJINnMzz/gPulpoFFGymNwrsMcw==", "license": "MIT", + "peer": true, "dependencies": { "@emotion/is-prop-valid": "1.2.2", "@emotion/unitless": "0.8.1", @@ -24100,6 +24142,7 @@ "node_modules/terser-webpack-plugin/node_modules/ajv": { "version": "6.12.6", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -24528,6 +24571,7 @@ "version": "10.9.1", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -24573,7 +24617,8 @@ }, "node_modules/tslib": { "version": "2.8.0", - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/tty-browserify": { "version": "0.0.1", @@ -25054,6 +25099,7 @@ "node_modules/url-loader/node_modules/ajv": { "version": "6.12.6", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -25386,6 +25432,7 @@ "node_modules/webpack": { "version": "5.95.0", "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "^1.0.5", "@webassemblyjs/ast": "^1.12.1", @@ -25666,6 +25713,7 @@ "node_modules/webpack/node_modules/ajv": { "version": "6.12.6", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -26189,6 +26237,7 @@ "version": "4.1.12", "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/sidebar.ts b/src/sidebar.ts index a54af94256..73aec49ab1 100644 --- a/src/sidebar.ts +++ b/src/sidebar.ts @@ -607,7 +607,6 @@ const kratos: SidebarItemsConfig = [ "kratos/guides/docker", "kratos/guides/deploy-kratos-example", "kratos/guides/upgrade", - "kratos/guides/normalize-phone-numbers", "kratos/guides/production", "kratos/guides/multi-tenancy-multitenant", "self-hosted/operations/scalability", From 9777f303dfbf1e8625aadfedcb5faa1ea263bda9 Mon Sep 17 00:00:00 2001 From: Henning Perl Date: Tue, 14 Apr 2026 20:12:30 +0200 Subject: [PATCH 4/7] fix tests --- .../kratos/guides/normalize-phone-numbers.mdx | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 docs/kratos/guides/normalize-phone-numbers.mdx diff --git a/docs/kratos/guides/normalize-phone-numbers.mdx b/docs/kratos/guides/normalize-phone-numbers.mdx new file mode 100644 index 0000000000..0642381fd4 --- /dev/null +++ b/docs/kratos/guides/normalize-phone-numbers.mdx @@ -0,0 +1,141 @@ +--- +id: normalize-phone-numbers +title: Normalize phone numbers to E.164 +sidebar_label: Normalize phone numbers +--- + +Starting with this release, Ory Kratos normalizes phone numbers to +[E.164 format](https://en.wikipedia.org/wiki/E.164) when they're used as +identifiers, verifiable addresses, or recovery addresses. New data is normalized +on write. Existing data continues to work through a backward-compatible lookup, +but you should run the `normalize-phone-numbers` migration command after +upgrading to converge all rows to E.164. + +This guide is for self-hosted Kratos administrators (OSS and OEL). Ory Network +customers don't need to take any action. + +:::info + +Back up your database before running the migration. + +::: + +## Why normalize + +Before this change, Kratos stored phone numbers exactly as users entered them. +A user who registered with `+49 176 671 11 638` and another who registered with +`+4917667111638` would create two separate identities for the same phone number. +Lookups, recovery, and verification could behave inconsistently depending on the +input format. + +After normalization, all phone numbers are stored in E.164 format +(for example, `+4917667111638`). Lookups match regardless of how the user +formatted the input. + +## Rollout sequence + +Run the steps in this exact order: + +1. **Deploy the new Kratos version.** + The new code normalizes phone numbers on write and uses a backward-compatible + lookup that matches both E.164 and legacy formats. Existing users can still + log in with whatever format they originally registered with. + +2. **Run the migration command.** + After the deploy completes and traffic is stable, run: + + ``` + kratos migrate normalize-phone-numbers + ``` + + Or with the DSN from the environment: + + ``` + export DSN=... + kratos migrate normalize-phone-numbers -e + ``` + + The command iterates over `identity_credential_identifiers`, + `identity_verifiable_addresses`, and `identity_recovery_addresses` and + rewrites any non-E.164 phone numbers in place. + +:::caution + +Don't run the migration before deploying the new Kratos version. The previous +version does exact-string matching on identifiers. If you normalize the database +first, users who type their phone number in the original (non-E.164) format +won't be able to log in until the new code is deployed. + +::: + +## What the command does + +The command uses keyset pagination to scan three tables in batches: + +| Table | Column | Filter | +|-------|--------|--------| +| `identity_credential_identifiers` | `identifier` | `identifier LIKE '+%'` | +| `identity_verifiable_addresses` | `value` | `via = 'sms'` | +| `identity_recovery_addresses` | `value` | `via = 'sms'` | + +For each row, the command parses the value with the +[`nyaruka/phonenumbers`](https://github.com/nyaruka/phonenumbers) library and +rewrites it to E.164 if parsing succeeds. Rows that fail to parse (for example, +an OIDC subject that happens to start with `+`) are left untouched and counted +as skipped. + +The command is **idempotent**: running it twice is safe. The second run only +reports skipped rows. + +## Flags + +| Flag | Default | Description | +|------|---------|-------------| +| `-e`, `--read-from-env` | `false` | Read the database connection string from the `DSN` environment variable. | +| `-b`, `--batch-size` | `1000` | Number of rows to process per batch. | +| `--dry-run` | `false` | Report what would change without writing. | + +Use `--dry-run` first to preview the changes: + +``` +kratos migrate normalize-phone-numbers --dry-run -e +``` + +Each row that would be updated is printed in the form: + +``` +[dry-run] identity_credential_identifiers : "+49 176 671 11 638" -> "+4917667111638" +``` + +## Output + +After processing all three tables, the command prints a summary: + +``` +=== Summary === +identity_credential_identifiers: scanned=1234 updated=42 skipped=1192 errors=0 +identity_verifiable_addresses: scanned=987 updated=15 skipped=972 errors=0 +identity_recovery_addresses: scanned=987 updated=15 skipped=972 errors=0 +``` + +- `scanned`: rows examined. +- `updated`: rows rewritten to E.164 (or rows that *would* be rewritten in dry-run mode). +- `skipped`: rows already in E.164 format, or values that aren't valid phone numbers. +- `errors`: rows that failed to update. Errors are logged to stderr with the row ID and source value. + +## Duplicate handling + +If the migration finds two rows that normalize to the same E.164 value (for +example, `+49 176 671 11 638` and `+4917667111638` for the same user), the +update fails on the second row with a unique constraint violation, which the +command logs as an error and skips. You can resolve the duplicate manually and +re-run the command. + +In practice, duplicates are rare. Most identities have only one phone identifier +per credential type. + +## Rolling back + +The migration only converts non-E.164 values to E.164. It doesn't store the +original value, so there's no automatic rollback. If you need to revert, restore +from the backup you took before running the command. From 222ae4ee36ac403ba9a070ec704388bd3677ffbe Mon Sep 17 00:00:00 2001 From: Henning Perl Date: Wed, 15 Apr 2026 07:18:22 +0200 Subject: [PATCH 5/7] chore: format --- .../kratos/guides/normalize-phone-numbers.mdx | 88 ++++++++----------- 1 file changed, 36 insertions(+), 52 deletions(-) diff --git a/docs/kratos/guides/normalize-phone-numbers.mdx b/docs/kratos/guides/normalize-phone-numbers.mdx index 0642381fd4..58405c6d41 100644 --- a/docs/kratos/guides/normalize-phone-numbers.mdx +++ b/docs/kratos/guides/normalize-phone-numbers.mdx @@ -4,15 +4,12 @@ title: Normalize phone numbers to E.164 sidebar_label: Normalize phone numbers --- -Starting with this release, Ory Kratos normalizes phone numbers to -[E.164 format](https://en.wikipedia.org/wiki/E.164) when they're used as -identifiers, verifiable addresses, or recovery addresses. New data is normalized -on write. Existing data continues to work through a backward-compatible lookup, -but you should run the `normalize-phone-numbers` migration command after -upgrading to converge all rows to E.164. +Starting with this release, Ory Kratos normalizes phone numbers to [E.164 format](https://en.wikipedia.org/wiki/E.164) when +they're used as identifiers, verifiable addresses, or recovery addresses. New data is normalized on write. Existing data continues +to work through a backward-compatible lookup, but you should run the `normalize-phone-numbers` migration command after upgrading +to converge all rows to E.164. -This guide is for self-hosted Kratos administrators (OSS and OEL). Ory Network -customers don't need to take any action. +This guide is for self-hosted Kratos administrators (OSS and OEL). Ory Network customers don't need to take any action. :::info @@ -22,24 +19,20 @@ Back up your database before running the migration. ## Why normalize -Before this change, Kratos stored phone numbers exactly as users entered them. -A user who registered with `+49 176 671 11 638` and another who registered with -`+4917667111638` would create two separate identities for the same phone number. -Lookups, recovery, and verification could behave inconsistently depending on the -input format. +Before this change, Kratos stored phone numbers exactly as users entered them. A user who registered with `+49 176 671 11 638` and +another who registered with `+4917667111638` would create two separate identities for the same phone number. Lookups, recovery, +and verification could behave inconsistently depending on the input format. -After normalization, all phone numbers are stored in E.164 format -(for example, `+4917667111638`). Lookups match regardless of how the user -formatted the input. +After normalization, all phone numbers are stored in E.164 format (for example, `+4917667111638`). Lookups match regardless of how +the user formatted the input. ## Rollout sequence Run the steps in this exact order: 1. **Deploy the new Kratos version.** - The new code normalizes phone numbers on write and uses a backward-compatible - lookup that matches both E.164 and legacy formats. Existing users can still - log in with whatever format they originally registered with. + The new code normalizes phone numbers on write and uses a backward-compatible lookup that matches both E.164 and legacy + formats. Existing users can still log in with whatever format they originally registered with. 2. **Run the migration command.** After the deploy completes and traffic is stable, run: @@ -55,16 +48,14 @@ Run the steps in this exact order: kratos migrate normalize-phone-numbers -e ``` - The command iterates over `identity_credential_identifiers`, - `identity_verifiable_addresses`, and `identity_recovery_addresses` and - rewrites any non-E.164 phone numbers in place. + The command iterates over `identity_credential_identifiers`, `identity_verifiable_addresses`, and `identity_recovery_addresses` + and rewrites any non-E.164 phone numbers in place. :::caution -Don't run the migration before deploying the new Kratos version. The previous -version does exact-string matching on identifiers. If you normalize the database -first, users who type their phone number in the original (non-E.164) format -won't be able to log in until the new code is deployed. +Don't run the migration before deploying the new Kratos version. The previous version does exact-string matching on identifiers. +If you normalize the database first, users who type their phone number in the original (non-E.164) format won't be able to log in +until the new code is deployed. ::: @@ -72,28 +63,25 @@ won't be able to log in until the new code is deployed. The command uses keyset pagination to scan three tables in batches: -| Table | Column | Filter | -|-------|--------|--------| +| Table | Column | Filter | +| --------------------------------- | ------------ | ---------------------- | | `identity_credential_identifiers` | `identifier` | `identifier LIKE '+%'` | -| `identity_verifiable_addresses` | `value` | `via = 'sms'` | -| `identity_recovery_addresses` | `value` | `via = 'sms'` | +| `identity_verifiable_addresses` | `value` | `via = 'sms'` | +| `identity_recovery_addresses` | `value` | `via = 'sms'` | -For each row, the command parses the value with the -[`nyaruka/phonenumbers`](https://github.com/nyaruka/phonenumbers) library and -rewrites it to E.164 if parsing succeeds. Rows that fail to parse (for example, -an OIDC subject that happens to start with `+`) are left untouched and counted -as skipped. +For each row, the command parses the value with the [`nyaruka/phonenumbers`](https://github.com/nyaruka/phonenumbers) library and +rewrites it to E.164 if parsing succeeds. Rows that fail to parse (for example, an OIDC subject that happens to start with `+`) +are left untouched and counted as skipped. -The command is **idempotent**: running it twice is safe. The second run only -reports skipped rows. +The command is **idempotent**: running it twice is safe. The second run only reports skipped rows. ## Flags -| Flag | Default | Description | -|------|---------|-------------| +| Flag | Default | Description | +| ----------------------- | ------- | ------------------------------------------------------------------------ | | `-e`, `--read-from-env` | `false` | Read the database connection string from the `DSN` environment variable. | -| `-b`, `--batch-size` | `1000` | Number of rows to process per batch. | -| `--dry-run` | `false` | Report what would change without writing. | +| `-b`, `--batch-size` | `1000` | Number of rows to process per batch. | +| `--dry-run` | `false` | Report what would change without writing. | Use `--dry-run` first to preview the changes: @@ -119,23 +107,19 @@ identity_recovery_addresses: scanned=987 updated=15 skipped=972 errors=0 ``` - `scanned`: rows examined. -- `updated`: rows rewritten to E.164 (or rows that *would* be rewritten in dry-run mode). +- `updated`: rows rewritten to E.164 (or rows that _would_ be rewritten in dry-run mode). - `skipped`: rows already in E.164 format, or values that aren't valid phone numbers. - `errors`: rows that failed to update. Errors are logged to stderr with the row ID and source value. ## Duplicate handling -If the migration finds two rows that normalize to the same E.164 value (for -example, `+49 176 671 11 638` and `+4917667111638` for the same user), the -update fails on the second row with a unique constraint violation, which the -command logs as an error and skips. You can resolve the duplicate manually and -re-run the command. +If the migration finds two rows that normalize to the same E.164 value (for example, `+49 176 671 11 638` and `+4917667111638` for +the same user), the update fails on the second row with a unique constraint violation, which the command logs as an error and +skips. You can resolve the duplicate manually and re-run the command. -In practice, duplicates are rare. Most identities have only one phone identifier -per credential type. +In practice, duplicates are rare. Most identities have only one phone identifier per credential type. ## Rolling back -The migration only converts non-E.164 values to E.164. It doesn't store the -original value, so there's no automatic rollback. If you need to revert, restore -from the backup you took before running the command. +The migration only converts non-E.164 values to E.164. It doesn't store the original value, so there's no automatic rollback. If +you need to revert, restore from the backup you took before running the command. From 6603e8c7c76895e5f1a75638fff3f93df7ef4733 Mon Sep 17 00:00:00 2001 From: Henning Perl Date: Wed, 15 Apr 2026 07:19:12 +0200 Subject: [PATCH 6/7] fix --- docs/kratos/guides/upgrade.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/kratos/guides/upgrade.mdx b/docs/kratos/guides/upgrade.mdx index 10ce345285..a075909859 100644 --- a/docs/kratos/guides/upgrade.mdx +++ b/docs/kratos/guides/upgrade.mdx @@ -17,7 +17,7 @@ Back up your data! Applying upgrades can lead to data loss if handled incorrectl 1. Update the [Ory Kratos SDK](../sdk/01_overview.md) if used in your application. 1. [Install](../install.mdx) the new version of Ory Kratos. 1. Run [`kratos migrate sql`](../cli/kratos-migrate-sql.md) to run the SQL migrations to the new database schema. -1. If you are upgrading to a version that introduces phone number normalization, run kratos migrate normalize-phone-numbers` after +1. If you are upgrading to a version that introduces phone number normalization, run `kratos migrate normalize-phone-numbers` after the new version is deployed and serving traffic. This rewrites existing phone identifiers to E.164 format. See the [phone normalization guide](./normalize-phone-numbers.mdx) for the recommended rollout sequence. From 3f743c7356d1a0810b358151556ec6c63486e165 Mon Sep 17 00:00:00 2001 From: Henning Perl Date: Wed, 15 Apr 2026 07:19:31 +0200 Subject: [PATCH 7/7] chore: format --- docs/kratos/guides/upgrade.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/kratos/guides/upgrade.mdx b/docs/kratos/guides/upgrade.mdx index a075909859..ae5a351ed4 100644 --- a/docs/kratos/guides/upgrade.mdx +++ b/docs/kratos/guides/upgrade.mdx @@ -17,8 +17,8 @@ Back up your data! Applying upgrades can lead to data loss if handled incorrectl 1. Update the [Ory Kratos SDK](../sdk/01_overview.md) if used in your application. 1. [Install](../install.mdx) the new version of Ory Kratos. 1. Run [`kratos migrate sql`](../cli/kratos-migrate-sql.md) to run the SQL migrations to the new database schema. -1. If you are upgrading to a version that introduces phone number normalization, run `kratos migrate normalize-phone-numbers` after - the new version is deployed and serving traffic. This rewrites existing phone identifiers to E.164 format. See the +1. If you are upgrading to a version that introduces phone number normalization, run `kratos migrate normalize-phone-numbers` + after the new version is deployed and serving traffic. This rewrites existing phone identifiers to E.164 format. See the [phone normalization guide](./normalize-phone-numbers.mdx) for the recommended rollout sequence. Should you run into problems with the upgrade, consider a stepped upgrade and please visit the community