├── .github ├── pull_request_template.md └── workflows │ └── release.yml ├── .gitignore ├── .goreleaser.yaml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── cli ├── decode.go ├── encode.go ├── new.go └── root.go ├── devbox.json ├── devbox.lock ├── go.mod ├── go.sum ├── main.go ├── scripts └── update_specs.sh └── spec ├── README.md ├── invalid.json ├── invalid.yml ├── valid.json └── valid.yml /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # ⚠️ Submit PRs in our opensource monorepo instead ⚠️ 2 | 3 | This repository is automatically published from our opensource monorepo: 4 | https://github.com/jetify-com/opensource 5 | 6 | If you want to contribute code changes to this project, please submit your 7 | PR via the monorepo. 8 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | concurrency: 4 | group: ${{ github.workflow }}-${{ github.ref || github.run_id }} 5 | cancel-in-progress: true 6 | 7 | on: 8 | # Build/Release on demand 9 | workflow_dispatch: 10 | push: 11 | tags: 12 | - "*" # Tags that trigger a new release version 13 | 14 | permissions: 15 | contents: write 16 | pull-requests: read 17 | 18 | jobs: 19 | release: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Checkout Monorepo 23 | uses: actions/checkout@v4 24 | 25 | - name: Set up Go 26 | uses: actions/setup-go@v5 27 | with: 28 | go-version-file: ./go.mod 29 | 30 | - name: Release with goreleaser 31 | uses: goreleaser/goreleaser-action@v5 32 | with: 33 | distribution: goreleaser 34 | version: latest 35 | args: release --clean 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | project_name: typeid 2 | 3 | before: 4 | hooks: 5 | - go mod tidy 6 | 7 | builds: 8 | - main: ./main.go 9 | binary: typeid 10 | flags: 11 | - -trimpath 12 | mod_timestamp: "{{ .CommitTimestamp }}" # For reproducible builds 13 | ldflags: 14 | - -s -w # Strip debug symbols 15 | env: 16 | - CGO_ENABLED=0 17 | - GO111MODULE=on 18 | goos: 19 | - darwin 20 | - linux 21 | - windows 22 | goarch: 23 | - "386" 24 | - amd64 25 | - arm 26 | - arm64 27 | ignore: 28 | - goos: darwin 29 | goarch: "386" 30 | 31 | archives: 32 | - files: 33 | - no-files-will-match-* # Glob that does not match to create archive with only binaries. 34 | name_template: "{{ .ProjectName }}_{{ .Tag }}_{{ .Os }}_{{ .Arch }}" 35 | 36 | checksum: 37 | name_template: "checksums.txt" 38 | algorithm: sha256 39 | 40 | release: 41 | prerelease: auto 42 | draft: true 43 | github: 44 | owner: jetify-com 45 | name: typeid 46 | 47 | snapshot: 48 | name_template: "{{ .Tag }}-devel" 49 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement. Use the 63 | "Report to repository admins" functionality on GitHub to report. 64 | 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series of 87 | actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or permanent 94 | ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within the 114 | community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.1, available at 120 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 121 | 122 | Community Impact Guidelines were inspired by 123 | [Mozilla's code of conduct enforcement ladder][mozilla coc]. 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | [https://www.contributor-covenant.org/faq][faq]. Translations are available at 127 | [https://www.contributor-covenant.org/translations][translations]. 128 | 129 | [homepage]: https://www.contributor-covenant.org 130 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 131 | [mozilla coc]: https://github.com/mozilla/diversity 132 | [faq]: https://www.contributor-covenant.org/faq 133 | [translations]: https://www.contributor-covenant.org/translations 134 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please describe the change you wish to 4 | make via a related issue, or a pull request. 5 | 6 | Please note we have a [code of conduct](CODE_OF_CONDUCT.md), please follow it in 7 | all your interactions with the project. 8 | 9 | ## Opening a Pull Request 10 | 11 | This project is published as a standalone repo from our 12 | [opensource monorepo](https://github.com/jetify-com/opensource). Pull requests 13 | should be sent to the monorepo instead, and they will automatically be published 14 | to this repo when merged. 15 | 16 | Contributions made to this project must be made under the terms of the 17 | [Apache 2 License](https://www.apache.org/licenses/LICENSE-2.0). 18 | By contributing to this project you agree to the terms stated in the 19 | [Community Contribution License](https://github.com/jetify-com/opensource/blob/main/CONTRIBUTING.md#community-contribution-license). 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TypeID 2 | 3 | ### A type-safe, K-sortable, globally unique identifier inspired by Stripe IDs 4 | 5 | ![License: Apache 2.0](https://img.shields.io/github/license/jetify-com/typeid) 6 | [![Join Discord](https://img.shields.io/discord/903306922852245526?color=7389D8&label=discord&logo=discord&logoColor=ffffff)](https://discord.gg/jetify) 7 | [![Built with Devbox](https://www.jetify.com/img/devbox/shield_galaxy.svg)](https://www.jetify.com/devbox/) 8 | 9 | ## What is it? 10 | 11 | TypeIDs are a modern, type-safe extension of UUIDv7. Inspired by a similar use 12 | of prefixes in Stripe's APIs. 13 | 14 | TypeIDs are canonically encoded as lowercase strings consisting of three parts: 15 | 16 | 1. A type prefix (at most 63 characters in all lowercase snake_case ASCII 17 | [a-z_]). 18 | 2. An underscore '\_' separator 19 | 3. A 128-bit UUIDv7 encoded as a 26-character string using a modified base32 20 | encoding. 21 | 22 | Here's an example of a TypeID of type `user`: 23 | 24 | ``` 25 | user_2x4y6z8a0b1c2d3e4f5g6h7j8k 26 | └──┘ └────────────────────────┘ 27 | type uuid suffix (base32) 28 | ``` 29 | 30 | A [formal specification](./spec) defines the encoding in more detail. 31 | 32 | ## Online Converter 33 | 34 | You can try converting UUID to TypeID and back using Jetify's TypeID Converter. 35 | Paste your TypeID string to convert to UUID or put your prefix and UUID in this 36 | format: `prefix:UUID` to convert to TypeID. 37 | 38 | ### [jetify.com/typeid](https://www.jetify.com/typeid) 39 | 40 | ## Benefits 41 | 42 | - **Type-safe:** you can't accidentally use a `user` ID where a `post` ID is 43 | expected. When debugging, you can immediately understand what type of entity a 44 | TypeID refers to thanks to the type prefix. 45 | - **Compatible with UUIDs:** TypeIDs are a superset of UUIDs. They are based on 46 | the upcoming 47 | [UUIDv7 standard](https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html#name-uuid-version-7). 48 | If you decode the TypeID and remove the type information, you get a valid 49 | UUIDv7. 50 | - **K-Sortable**: TypeIDs are K-sortable and can be used as the primary key in a 51 | database while ensuring good locality. Compare to entirely random global ids, 52 | like UUIDv4, that generally suffer from poor database locality. 53 | - **Thoughtful encoding**: the base32 encoding is URL safe, case-insensitive, 54 | avoids ambiguous characters, can be selected for copy-pasting by 55 | double-clicking, and is a more compact encoding than the traditional hex 56 | encoding used by UUIDs (26 characters vs 36 characters). 57 | 58 | ## Implementations 59 | 60 | Implementations should adhere to the formal [specification](./spec). 61 | 62 | Latest spec version: v0.3.0 63 | 64 | ### Official Implementations by `jetify` 65 | 66 | | Language | Status | Spec Version | 67 | | ----------------------------------------------------- | ------------- | ------------ | 68 | | [Go](https://github.com/jetify-com/typeid-go) | ✓ Implemented | v0.3 | 69 | | [SQL](https://github.com/jetify-com/typeid-sql) | ✓ Implemented | v0.2 | 70 | | [TypeScript](https://github.com/jetify-com/typeid-js) | ✓ Implemented | v0.3 | 71 | 72 | ### Community Provided Implementations 73 | 74 | | Language | Author | Spec Version | 75 | | ------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | 76 | | [C# (.Net)](https://github.com/TenCoKaciStromy/typeid-dotnet) | [@TenCoKaciStromy](https://github.com/TenCoKaciStromy) | v0.2 on 2023-06-30 | 77 | | [C# (.Net Standard 2.1)](https://github.com/cbuctok/typeId) | [@cbuctok](https://github.com/cbuctok) | v0.2 on 2023-07-03 | 78 | | [C# (.NET)](https://github.com/firenero/TypeId) | [@firenero](https://github.com/firenero) | v0.3 on 2024-04-15 | 79 | | [Dart](https://github.com/TBD54566975/typeid-dart) | [@mistermoe](https://github.com/mistermoe) [@tbd54566975](https://github.com/tbd54566975) | [v0.3 on 2024-07-02](https://github.com/TBD54566975/typeid-dart/actions/runs/9755701869/job/26924658060#step:6:10) | 80 | | [Elixir](https://github.com/sloanelybutsurely/typeid-elixir) | [@sloanelybutsurely](https://github.com/sloanelybutsurely) | v0.3 on 2024-04-22 | 81 | | [Elixir](https://github.com/xinz/elixir_typeid) | [@xinz](https://github.com/xinz) | v0.1 on 2024-06-03 | 82 | | [Erlang](https://github.com/eproxus/keysmith) | [@eproxus](https://github.com/eproxus) | v0.3 on 2024-09-25 | 83 | | [Haskell](https://github.com/MMZK1526/mmzk-typeid) | [@MMZK1526](https://github.com/MMZK1526) | v0.3 on 2024-04-19 | 84 | | [Java](https://github.com/fxlae/typeid-java) | [@fxlae](https://github.com/fxlae) | v0.3 on 2024-04-14 | 85 | | [Java](https://github.com/softprops/typeid-java) | [@softprops](https://github.com/softprops) | v0.2 on 2023-07-04 | 86 | | [Kotlin](https://github.com/aleris/typeid-kotlin) | [@aleris](https://github.com/aleris) | v0.3 on 2024-05-18 | 87 | | [Lua](https://github.com/pushcx/typeid-lua) | [@pushcx](https://github.com/pushcx) | v0.3 on 2025-05-21 | 88 | | [OCaml](https://github.com/titouancreach/typeid-ocaml) | [@titouancreach](https://github.com/titouancreach) | v0.3 on 2024-04-22 | 89 | | [PHP](https://github.com/basecodeoy/typeid) | [@BaseCodeOy](https://github.com/basecodeoy) | v0.3 on 2024-12-06 | 90 | | [Postgres](https://github.com/blitss/typeid-postgres) | [@blitss](https://github.com/blitss) | [v0.3 on 2024-06-24](https://github.com/blitss/typeid-postgres/actions/runs/9637303320/job/26576304134#step:11:288) | 91 | | [Python](https://github.com/akhundMurad/typeid-python) | [@akhundMurad](https://github.com/akhundMurad) | [v0.3 on 2024-04-19](https://github.com/akhundMurad/typeid-python/releases/tag/v0.3.0) | 92 | | [Ruby](https://github.com/broothie/typeid-ruby) | [@broothie](https://github.com/broothie) | [v0.3 on 2024-04-13](https://github.com/broothie/typeid-ruby/pull/17) | 93 | | [Rust](https://github.com/conradludgate/type-safe-id) | [@conradludgate](https://github.com/conradludgate) | [v0.3 on 2024-04-12](https://github.com/conradludgate/type-safe-id/pull/1) | 94 | | [Rust](https://github.com/johnnynotsolucky/strong_id) | [@johnnynotsolucky](https://github.com/johnnynotsolucky) | [v0.3 on 2024-05-17](https://github.com/johnnynotsolucky/strong_id/commit/10aa50487bbdd851c58a2ed73071a50452441370) | 95 | | [Rust](https://github.com/Govcraft/mti) | [@Govcraft](https://github.com/Govcraft) | [v0.3 on 2024-07-15](https://github.com/Govcraft/mti/actions/runs/9945923733) | 96 | | [Scala](https://github.com/ant8e/uuid4cats-effect) | [@ant8e](https://github.com/ant8e) | v0.3 on 2024-04-19 | 97 | | [Scala](https://github.com/guizmaii-opensource/zio-uuid) | [@guizmaii](https://github.com/guizmaii) | Not validated yet | 98 | | [Swift](https://github.com/Frizlab/swift-typeid) | [@Frizlab](https://github.com/Frizlab) | v0.3 on 2024-04-19 | 99 | | [T-SQL](https://github.com/uniteeio/typeid_tsql) | [@uniteeio](https://github.com/uniteeio) | v0.2 on 2023-08-25 | 100 | | [TypeScript](https://github.com/ongteckwu/typeid-ts) | [@ongteckwu](https://github.com/ongteckwu) | v0.2 on 2023-06-30 | 101 | | [Zig](https://github.com/nikoksr/typeid-zig) | [@nikoksr](https://github.com/nikoksr) | v0.3 on 2024-12-29 | 102 | 103 | We are looking for community contributions to implement TypeIDs in other 104 | languages. 105 | 106 | ## Command-line Tool 107 | 108 | This repo includes a command-line tool for generating TypeIDs. To install it, 109 | run: 110 | 111 | ```bash 112 | curl -fsSL https://get.jetify.com/typeid | bash 113 | ``` 114 | 115 | To generate a new TypeID, run: 116 | 117 | ```console 118 | $ typeid new prefix 119 | prefix_01h2xcejqtf2nbrexx3vqjhp41 120 | ``` 121 | 122 | To decode an existing TypeID into a UUID run: 123 | 124 | ```console 125 | $ typeid decode prefix_01h2xcejqtf2nbrexx3vqjhp41 126 | type: prefix 127 | uuid: 0188bac7-4afa-78aa-bc3b-bd1eef28d881 128 | ``` 129 | 130 | And to encode an existing UUID into a TypeID run: 131 | 132 | ```console 133 | $ typeid encode prefix 0188bac7-4afa-78aa-bc3b-bd1eef28d881 134 | prefix_01h2xcejqtf2nbrexx3vqjhp41 135 | ``` 136 | 137 | ## Related Work 138 | 139 | - [UUIDv7](https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html#name-uuid-version-7) - 140 | The upcoming UUID standard that TypeIDs are based on. 141 | 142 | Alternatives to UUIDv7 that are also worth considering (but not type-safe like 143 | TypeIDs): 144 | 145 | - [xid](https://github.com/rs/xid) 146 | - [ulid](https://github.com/ulid) 147 | - [ksuid](https://github.com/segmentio/ksuid) 148 | -------------------------------------------------------------------------------- /cli/decode.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "go.jetify.com/typeid" 6 | ) 7 | 8 | func DecodeCmd() *cobra.Command { 9 | command := &cobra.Command{ 10 | Use: "decode ", 11 | Args: cobra.ExactArgs(1), 12 | Short: "Decode the given TypeID into a UUID", 13 | RunE: decodeCmd, 14 | SilenceErrors: true, 15 | SilenceUsage: true, 16 | } 17 | 18 | return command 19 | } 20 | 21 | func decodeCmd(cmd *cobra.Command, args []string) error { 22 | tid, err := typeid.Parse(args[0]) 23 | if err != nil { 24 | return err 25 | } 26 | cmd.Printf("type: %s\n", tid.Prefix()) 27 | cmd.Printf("uuid: %s\n", tid.UUID()) 28 | return nil 29 | } 30 | -------------------------------------------------------------------------------- /cli/encode.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "go.jetify.com/typeid" 6 | ) 7 | 8 | func EncodeCmd() *cobra.Command { 9 | command := &cobra.Command{ 10 | Use: "encode [] ", 11 | Args: cobra.RangeArgs(1, 2), 12 | Short: "Encode the given UUID into a TypeID using the given type prefix", 13 | RunE: encodeCmd, 14 | SilenceErrors: true, 15 | SilenceUsage: true, 16 | } 17 | 18 | return command 19 | } 20 | 21 | func encodeCmd(cmd *cobra.Command, args []string) error { 22 | prefix := "" 23 | uuid := "" 24 | if len(args) == 1 { 25 | uuid = args[0] 26 | } else { 27 | prefix = args[0] 28 | uuid = args[1] 29 | } 30 | tid, err := typeid.FromUUID(prefix, uuid) 31 | if err != nil { 32 | return err 33 | } 34 | cmd.Println(tid) 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /cli/new.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/spf13/cobra" 7 | "go.jetify.com/typeid" 8 | ) 9 | 10 | func NewCmd() *cobra.Command { 11 | command := &cobra.Command{ 12 | Use: "new []", 13 | Args: cobra.MaximumNArgs(1), 14 | Short: "Generate a new TypeID using the given type prefix", 15 | RunE: newCmd, 16 | SilenceErrors: true, 17 | SilenceUsage: true, 18 | } 19 | 20 | return command 21 | } 22 | 23 | func newCmd(cmd *cobra.Command, args []string) error { 24 | prefix := "" 25 | if len(args) > 0 { 26 | prefix = strings.ToLower(args[0]) 27 | } 28 | tid, err := typeid.Generate(prefix) 29 | if err != nil { 30 | return err 31 | } 32 | cmd.Println(tid) 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /cli/root.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func RootCmd() *cobra.Command { 12 | command := &cobra.Command{ 13 | Use: "typeid", 14 | Short: "Type-safe, K-sortable, globally unique identifiers", 15 | RunE: func(cmd *cobra.Command, args []string) error { 16 | return cmd.Help() 17 | }, 18 | SilenceErrors: true, 19 | SilenceUsage: true, 20 | } 21 | command.AddCommand(NewCmd()) 22 | command.AddCommand(EncodeCmd()) 23 | command.AddCommand(DecodeCmd()) 24 | 25 | return command 26 | } 27 | 28 | func Execute(ctx context.Context, args []string) int { 29 | cmd := RootCmd() 30 | cmd.SetArgs(args) 31 | err := cmd.ExecuteContext(ctx) 32 | if err != nil { 33 | fmt.Printf("[Error] %v\n", err) 34 | return 1 35 | } 36 | return 0 37 | } 38 | 39 | func Main() { 40 | code := Execute(context.Background(), os.Args[1:]) 41 | os.Exit(code) 42 | } 43 | -------------------------------------------------------------------------------- /devbox.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": ["go@latest", "golangci-lint@latest"], 3 | "shell": { 4 | "init_hook": ["export \"GOROOT=$(go env GOROOT)\""], 5 | "scripts": { 6 | "build": "go build -o dist/typeid", 7 | "lint": "golangci-lint run -c ../.golangci.yml", 8 | "test": "go test -race -cover ./..." 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /devbox.lock: -------------------------------------------------------------------------------- 1 | { 2 | "lockfile_version": "1", 3 | "packages": { 4 | "github:NixOS/nixpkgs/nixpkgs-unstable": { 5 | "resolved": "github:NixOS/nixpkgs/02032da4af073d0f6110540c8677f16d4be0117f?lastModified=1741037377&narHash=sha256-SvtvVKHaUX4Owb%2BPasySwZsoc5VUeTf1px34BByiOxw%3D" 6 | }, 7 | "go@latest": { 8 | "last_modified": "2025-02-12T00:10:52Z", 9 | "resolved": "github:NixOS/nixpkgs/83a2581c81ff5b06f7c1a4e7cc736a455dfcf7b4#go_1_24", 10 | "source": "devbox-search", 11 | "version": "1.24.0", 12 | "systems": { 13 | "aarch64-darwin": { 14 | "outputs": [ 15 | { 16 | "name": "out", 17 | "path": "/nix/store/qldcnifalkvyah0wnv7m4zb854yd9l88-go-1.24.0", 18 | "default": true 19 | } 20 | ], 21 | "store_path": "/nix/store/qldcnifalkvyah0wnv7m4zb854yd9l88-go-1.24.0" 22 | }, 23 | "aarch64-linux": { 24 | "outputs": [ 25 | { 26 | "name": "out", 27 | "path": "/nix/store/rrxgml7w4pfmibjbspkdvrw8vd2vnarb-go-1.24.0", 28 | "default": true 29 | } 30 | ], 31 | "store_path": "/nix/store/rrxgml7w4pfmibjbspkdvrw8vd2vnarb-go-1.24.0" 32 | }, 33 | "x86_64-darwin": { 34 | "outputs": [ 35 | { 36 | "name": "out", 37 | "path": "/nix/store/7imv22pl4qrjwvi6jzlfb305rc2min45-go-1.24.0", 38 | "default": true 39 | } 40 | ], 41 | "store_path": "/nix/store/7imv22pl4qrjwvi6jzlfb305rc2min45-go-1.24.0" 42 | }, 43 | "x86_64-linux": { 44 | "outputs": [ 45 | { 46 | "name": "out", 47 | "path": "/nix/store/vh5d5bj1sljdhdypy80x1ydx2jx6rv2q-go-1.24.0", 48 | "default": true 49 | } 50 | ], 51 | "store_path": "/nix/store/vh5d5bj1sljdhdypy80x1ydx2jx6rv2q-go-1.24.0" 52 | } 53 | } 54 | }, 55 | "golangci-lint@latest": { 56 | "last_modified": "2025-02-16T21:44:05Z", 57 | "resolved": "github:NixOS/nixpkgs/f0204ef4baa3b6317dee1c84ddeffbd293638836#golangci-lint", 58 | "source": "devbox-search", 59 | "version": "1.64.5", 60 | "systems": { 61 | "aarch64-darwin": { 62 | "outputs": [ 63 | { 64 | "name": "out", 65 | "path": "/nix/store/jh2f466rbi0pgk6f3w8jdzy4qyccybz3-golangci-lint-1.64.5", 66 | "default": true 67 | } 68 | ], 69 | "store_path": "/nix/store/jh2f466rbi0pgk6f3w8jdzy4qyccybz3-golangci-lint-1.64.5" 70 | }, 71 | "aarch64-linux": { 72 | "outputs": [ 73 | { 74 | "name": "out", 75 | "path": "/nix/store/63mvzwlqana7bfcy8jzmn3fvkn46k0p6-golangci-lint-1.64.5", 76 | "default": true 77 | } 78 | ], 79 | "store_path": "/nix/store/63mvzwlqana7bfcy8jzmn3fvkn46k0p6-golangci-lint-1.64.5" 80 | }, 81 | "x86_64-darwin": { 82 | "outputs": [ 83 | { 84 | "name": "out", 85 | "path": "/nix/store/d662i9k8p2alplmxc3bqypc646x7wy1b-golangci-lint-1.64.5", 86 | "default": true 87 | } 88 | ], 89 | "store_path": "/nix/store/d662i9k8p2alplmxc3bqypc646x7wy1b-golangci-lint-1.64.5" 90 | }, 91 | "x86_64-linux": { 92 | "outputs": [ 93 | { 94 | "name": "out", 95 | "path": "/nix/store/25732rsdh49iwjrik69sb9cfhiza00b5-golangci-lint-1.64.5", 96 | "default": true 97 | } 98 | ], 99 | "store_path": "/nix/store/25732rsdh49iwjrik69sb9cfhiza00b5-golangci-lint-1.64.5" 100 | } 101 | } 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module go.jetify.com/typeid-cli 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/spf13/cobra v1.9.1 7 | go.jetify.com/typeid v1.3.1-0.20250306202323-dd4ff4ba4012 8 | ) 9 | 10 | require ( 11 | github.com/gofrs/uuid/v5 v5.3.2 // indirect 12 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 13 | github.com/spf13/pflag v1.0.6 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/gofrs/uuid/v5 v5.3.2 h1:2jfO8j3XgSwlz/wHqemAEugfnTlikAYHhnqQ8Xh4fE0= 5 | github.com/gofrs/uuid/v5 v5.3.2/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= 6 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 7 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 8 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 9 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 10 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 11 | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= 12 | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 13 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 14 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 15 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 16 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 17 | go.jetify.com/typeid v1.3.1-0.20250306202323-dd4ff4ba4012 h1:bY2vZJl/hATzxnq6Y7tdVfKTn+NG0Bsf8tuaB/NgrUU= 18 | go.jetify.com/typeid v1.3.1-0.20250306202323-dd4ff4ba4012/go.mod h1:9ydU/WVtIrPPCxnAw07PfirBuUqDFi+jJvnKeSrJ3ZU= 19 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 20 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 21 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 22 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 23 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 24 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "go.jetify.com/typeid-cli/cli" 4 | 5 | func main() { 6 | cli.Main() 7 | } 8 | -------------------------------------------------------------------------------- /scripts/update_specs.sh: -------------------------------------------------------------------------------- 1 | #! env bash 2 | 3 | echo "Convert to JSON" 4 | yq eval typeid/typeid/spec/valid.yml --tojson > typeid/typeid/spec/valid.json 5 | yq eval typeid/typeid/spec/invalid.yml --tojson > typeid/typeid/spec/invalid.json 6 | 7 | echo "Update typeid-go" 8 | cp typeid/typeid/spec/valid.yml typeid/typeid-go/testdata/valid.yml 9 | cp typeid/typeid/spec/invalid.yml typeid/typeid-go/testdata/invalid.yml 10 | 11 | echo "Update typeid-js" 12 | cat <<-TS > typeid/typeid-js/test/valid.ts 13 | // Data copied from the valid.yml spec file 14 | export default $(cat typeid/typeid/spec/valid.json) 15 | TS 16 | cat <<-TS > typeid/typeid-js/test/invalid.ts 17 | // Data copied from the invalid.yml spec file 18 | export default $(cat typeid/typeid/spec/invalid.json) 19 | TS 20 | -------------------------------------------------------------------------------- /spec/README.md: -------------------------------------------------------------------------------- 1 | # TypeID Specification (Version 0.3.0) 2 | 3 | ## Overview 4 | 5 | TypeIDs are a type-safe extension of UUIDv7, they encode UUIDs in base32 and add 6 | a type prefix. 7 | 8 | Here's an example of a TypeID of type `user`: 9 | 10 | ``` 11 | user_2x4y6z8a0b1c2d3e4f5g6h7j8k 12 | └──┘ └────────────────────────┘ 13 | type uuid suffix (base32) 14 | ``` 15 | 16 | This document formalizes the specification for TypeIDs. 17 | 18 | ## Specification 19 | 20 | A typeid consists of three parts: 21 | 22 | 1. A **type prefix**: a string denoting the type of the ID. The prefix should be 23 | at most 63 characters in all lowercase snake_case ASCII `[a-z_]`. 24 | 1. A **separator**: an underscore `_` character. The separator is omitted if the 25 | prefix is empty. 26 | 1. A **UUID suffix**: a 128-bit UUIDv7 encoded as a 26-character string in 27 | base32. 28 | 29 | ### Length Constraints 30 | 31 | The overall length of a TypeID is constrained by its components: 32 | 33 | - **Minimum length**: 26 characters (when there is no prefix, just the UUID 34 | suffix) 35 | - **Maximum length**: 90 characters (63 for prefix + 1 for separator + 26 for 36 | suffix) 37 | 38 | ### Type Prefix 39 | 40 | A type prefix is a string denoting the type of the ID. The prefix must: 41 | 42 | - Contain at most 63 characters. 43 | - May be empty. 44 | - If not empty: 45 | - Must contain only lowercase alphabetic ASCII characters `[a-z]`, or an 46 | underscore `_`. 47 | - Must start and end with an alphabetic character `[a-z]`. Underscores are not 48 | allowed at the beginning or end of the string. 49 | - **Digits and uppercase letters are not permitted.** 50 | - **Consecutive underscores (e.g., `foo__bar`) are allowed.** 51 | 52 | Valid prefixes match the following regex: `^([a-z]([a-z_]{0,61}[a-z])?)?$`. 53 | 54 | The empty string is a valid prefix, it's there for use cases in which 55 | applications need to encode a typeid but elide the type information. 56 | 57 | While short prefixes are technically allowed, for clarity and future 58 | scalability, applications SHOULD use a prefix that is at least 3 characters long 59 | (e.g., `usr_...` or `user_...` rather than `u_...`). 60 | 61 | ### Separator 62 | 63 | The separator is a single underscore character `_`. If the prefix is empty, the 64 | separator is omitted. 65 | 66 | ### UUID Suffix 67 | 68 | The UUID suffix encodes exactly 128-bits of data in 26 characters. It uses the 69 | base32 encoding described below. 70 | 71 | #### Base32 Encoding 72 | 73 | The UUID's 128 bits are treated in big-endian order (most significant bit 74 | first). Two zeroed bits are prepended to the left of these 128-bits, resulting 75 | in 130-bits of data. The 130-bits are then split from left to right into 26 76 | groups of 5 bits each. Each 5-bit group is encoded as a single character in the 77 | base32 alphabet, resulting in a total of 26 characters. 78 | 79 | In practice this is most often done by using bit-shifting and a lookup table. 80 | See the 81 | [reference implementation encoding](https://github.com/jetify-com/typeid-go/blob/main/base32/base32.go) 82 | for an example. 83 | 84 | Note that this is different from the standard base32 encoding which encodes in 85 | groups of 5 bytes (40 bits) and appends any padding at the end of the data. 86 | 87 | The encoding uses the following alphabet `0123456789abcdefghjkmnpqrstvwxyz` as 88 | specified by the following table: 89 | 90 | | Value | Symbol | Value | Symbol | Value | Symbol | Value | Symbol | 91 | | ----- | ------ | ----- | ------ | ----- | ------ | ----- | ------ | 92 | | 0 | 0 | 8 | 8 | 16 | g | 24 | r | 93 | | 1 | 1 | 9 | 9 | 17 | h | 25 | s | 94 | | 2 | 2 | 10 | a | 18 | j | 26 | t | 95 | | 3 | 3 | 11 | b | 19 | k | 27 | v | 96 | | 4 | 4 | 12 | c | 20 | m | 28 | w | 97 | | 5 | 5 | 13 | d | 21 | n | 29 | x | 98 | | 6 | 6 | 14 | e | 22 | p | 30 | y | 99 | | 7 | 7 | 15 | f | 23 | q | 31 | z | 100 | 101 | This is the same alphabet used by 102 | [Crockford's base32 encoding](https://www.crockford.com/base32.html), but in our 103 | case the alphabet encoding is strict: always in lowercase, no hyphens allowed, 104 | and we never decode multiple ambiguous characters to the same value. 105 | 106 | Technically speaking, 26 characters in base32 can encode 130 bits of data, but 107 | UUIDs are 128 bits. We therefore need to prevent overflow errors by not allowing 108 | typeids that would represent a value that exceeds 128 bits. 109 | 110 | Since the leading 2 bits of the 130-bit value are always zero (due to the two 111 | zeroed bits we prepend), the first 5-bit group (i.e., the first base32 112 | character) must never exceed decimal 7 (`0b0111`). Therefore, the maximum 113 | possible suffix for a typeid is `7zzzzzzzzzzzzzzzzzzzzzzzzz`, which corresponds 114 | to the maximum 128-bit value of a UUID (`0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF`). 115 | 116 | Implementations MUST reject any suffix where the first character is greater than 117 | `7`, as this would represent a value that exceeds 128 bits. The simplest way to 118 | validate this is by checking that the first character of the suffix is `7` or 119 | less. 120 | 121 | #### Compatibility with UUID 122 | 123 | When generating a new TypeID, the generated UUID suffix MUST decode to a valid 124 | UUIDv7. This means: 125 | 126 | - Bits 48-51 of the UUID MUST be `0111` (indicating version 7) 127 | - Bits 64-65 of the UUID MUST be `10` (indicating the UUID variant) 128 | 129 | Implementations SHOULD allow encoding/decoding of other UUID variants when the 130 | bits are provided by end users. This makes it possible for applications to 131 | encode other UUID variants like UUIDv1 or UUIDv4 at their discretion. 132 | 133 | ## Examples 134 | 135 | Here are additional examples of valid TypeIDs with different prefix patterns: 136 | 137 | | Prefix | Example TypeID | Notes | 138 | | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | ---------------------------------- | 139 | | _(empty)_ | `01h5fskfsk4fpeqwnsyz5hj55t` | No prefix, no separator | 140 | | `user` | `user_01h5fskfsk4fpeqwnsyz5hj55t` | Common use case | 141 | | `my_type` | `my_type_01h5fskfsk4fpeqwnsyz5hj55t` | Prefix with an underscore | 142 | | `a_b_c` | `a_b_c_01h5fskfsk4fpeqwnsyz5hj55t` | Multiple underscores | 143 | | `my__type` | `my__type_01h5fskfsk4fpeqwnsyz5hj55t` | Consecutive underscores | 144 | | `a` | `a_01h5fskfsk4fpeqwnsyz5hj55t` | Single-letter prefix (discouraged) | 145 | | `abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk` | `abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk_01h5fskfsk4fpeqwnsyz5hj55t` | Maximum length prefix (63 chars) | 146 | 147 | The following table shows examples of invalid TypeIDs that might appear valid at 148 | first glance: 149 | 150 | | Invalid TypeID Example | Reason Invalid | 151 | | ------------------------------------ | ----------------------------------------------------- | 152 | | `PREFIX_01h5fskfsk4fpeqwnsyz5hj55t` | Prefix contains uppercase letters (must be lowercase) | 153 | | `12345_01h5fskfsk4fpeqwnsyz5hj55t` | Prefix contains numbers (only [a-z_] allowed) | 154 | | `_prefix_01h5fskfsk4fpeqwnsyz5hj55t` | Prefix starts with underscore (must start with [a-z]) | 155 | | `prefix__01h5fskfsk4fpeqwnsyz5hj55t` | Prefix ends with underscore (must end with [a-z]) | 156 | | `prefix_0123456789ABCDEFGHJKMNPQRS` | Suffix contains uppercase letters (must be lowercase) | 157 | | `prefix_8zzzzzzzzzzzzzzzzzzzzzzzzz` | Suffix exceeds maximum value (first char must be ≤7) | 158 | 159 | For comprehensive testing, please refer to the full set of examples in the 160 | [valid.yml](valid.yml) and [invalid.yml](invalid.yml) files. These files contain 161 | numerous test cases to help validate implementations, including edge cases and 162 | corner cases not shown above. 163 | 164 | ## Versioning 165 | 166 | This spec uses semantic versioning: `MAJOR.MINOR.PATCH`. The version is 167 | incremented when the spec changes in a way that is not backwards compatible. 168 | 169 | Libraries that implement this spec should also use semantic versioning. 170 | 171 | ## Validating Implementations 172 | 173 | To assist library authors in validating their implementations, we provide: 174 | 175 | - A [reference implementation in Go](https://github.com/jetify-com/typeid-go) 176 | with extensive testing. 177 | - A [valid.yml](valid.yml) file containing a list of valid typeids along with 178 | their corresponding decoded UUIDs. For convenience, we also provide a 179 | [valid.json](valid.json) file containing the same data in JSON format.\ 180 | When implementing, ensure that: 181 | 1. Encoding the hex UUID produces the expected base32 suffix 182 | 2. Decoding the base32 suffix produces the original hex UUID 183 | - An [invalid.yml](invalid.yml) file containing a list of strings that are 184 | invalid typeids and should fail to parse/decode. For convenience, we also 185 | provide a [invalid.json](invalid.json) file containing the same data in JSON 186 | format.\ 187 | When implementing, ensure that your implementation rejects all invalid 188 | strings. 189 | -------------------------------------------------------------------------------- /spec/invalid.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "prefix-uppercase", 4 | "typeid": "PREFIX_00000000000000000000000000", 5 | "description": "The prefix should be lowercase with no uppercase letters" 6 | }, 7 | { 8 | "name": "prefix-numeric", 9 | "typeid": "12345_00000000000000000000000000", 10 | "description": "The prefix can't have numbers, it needs to be alphabetic" 11 | }, 12 | { 13 | "name": "prefix-period", 14 | "typeid": "pre.fix_00000000000000000000000000", 15 | "description": "The prefix can't have symbols, it needs to be alphabetic" 16 | }, 17 | { 18 | "name": "prefix-non-ascii", 19 | "typeid": "préfix_00000000000000000000000000", 20 | "description": "The prefix can only have ascii letters" 21 | }, 22 | { 23 | "name": "prefix-spaces", 24 | "typeid": " prefix_00000000000000000000000000", 25 | "description": "The prefix can't have any spaces" 26 | }, 27 | { 28 | "name": "prefix-64-chars", 29 | "typeid": "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl_00000000000000000000000000", 30 | "description": "The prefix can't be 64 characters, it needs to be 63 characters or less" 31 | }, 32 | { 33 | "name": "separator-empty-prefix", 34 | "typeid": "_00000000000000000000000000", 35 | "description": "If the prefix is empty, the separator should not be there" 36 | }, 37 | { 38 | "name": "separator-empty", 39 | "typeid": "_", 40 | "description": "A separator by itself should not be treated as the empty string" 41 | }, 42 | { 43 | "name": "suffix-short", 44 | "typeid": "prefix_1234567890123456789012345", 45 | "description": "The suffix can't be 25 characters, it needs to be exactly 26 characters" 46 | }, 47 | { 48 | "name": "suffix-long", 49 | "typeid": "prefix_123456789012345678901234567", 50 | "description": "The suffix can't be 27 characters, it needs to be exactly 26 characters" 51 | }, 52 | { 53 | "name": "suffix-spaces", 54 | "typeid": "prefix_1234567890123456789012345 ", 55 | "description": "The suffix can't have any spaces" 56 | }, 57 | { 58 | "name": "suffix-uppercase", 59 | "typeid": "prefix_0123456789ABCDEFGHJKMNPQRS", 60 | "description": "The suffix should be lowercase with no uppercase letters" 61 | }, 62 | { 63 | "name": "suffix-hyphens", 64 | "typeid": "prefix_123456789-123456789-123456", 65 | "description": "The suffix can't have any hyphens" 66 | }, 67 | { 68 | "name": "suffix-wrong-alphabet", 69 | "typeid": "prefix_ooooooiiiiiiuuuuuuulllllll", 70 | "description": "The suffix should only have letters from the spec's alphabet" 71 | }, 72 | { 73 | "name": "suffix-ambiguous-crockford", 74 | "typeid": "prefix_i23456789ol23456789oi23456", 75 | "description": "The suffix should not have any ambiguous characters from the crockford encoding" 76 | }, 77 | { 78 | "name": "suffix-hyphens-crockford", 79 | "typeid": "prefix_123456789-0123456789-0123456", 80 | "description": "The suffix can't ignore hyphens as in the crockford encoding" 81 | }, 82 | { 83 | "name": "suffix-overflow", 84 | "typeid": "prefix_8zzzzzzzzzzzzzzzzzzzzzzzzz", 85 | "description": "The suffix should encode at most 128-bits" 86 | }, 87 | { 88 | "name": "prefix-underscore-start", 89 | "typeid": "_prefix_00000000000000000000000000", 90 | "description": "The prefix can't start with an underscore" 91 | }, 92 | { 93 | "name": "prefix-underscore-end", 94 | "typeid": "prefix__00000000000000000000000000", 95 | "description": "The prefix can't end with an underscore" 96 | }, 97 | { 98 | "name": "empty", 99 | "typeid": "", 100 | "description": "The empty string is not a valid typeid" 101 | }, 102 | { 103 | "name": "prefix-empty", 104 | "typeid": "prefix_", 105 | "description": "The suffix can't be the empty string" 106 | } 107 | ] 108 | -------------------------------------------------------------------------------- /spec/invalid.yml: -------------------------------------------------------------------------------- 1 | # This file contains test data that should be treated as *invalid* TypeIDs by 2 | # conforming implementations. 3 | # 4 | # Each example contains an invalid TypeID string. Implementations are expected 5 | # to throw an error when attempting to parse/validate these strings. 6 | # 7 | # Last updated: 2024-05-18 (for version 0.3.0 of the spec) 8 | 9 | - name: prefix-uppercase 10 | typeid: "PREFIX_00000000000000000000000000" 11 | description: "The prefix should be lowercase with no uppercase letters" 12 | 13 | - name: prefix-numeric 14 | typeid: "12345_00000000000000000000000000" 15 | description: "The prefix can't have numbers, it needs to be alphabetic" 16 | 17 | - name: prefix-period 18 | typeid: "pre.fix_00000000000000000000000000" 19 | description: "The prefix can't have symbols, it needs to be alphabetic" 20 | 21 | # Test removed in v0.3.0 – we now allow underscores in the prefix 22 | # - name: prefix-underscore 23 | # typeid: "pre_fix_00000000000000000000000000" 24 | # description: "The prefix can't have symbols, it needs to be alphabetic" 25 | 26 | - name: prefix-non-ascii 27 | typeid: "préfix_00000000000000000000000000" 28 | description: "The prefix can only have ascii letters" 29 | 30 | - name: prefix-spaces 31 | typeid: " prefix_00000000000000000000000000" 32 | description: "The prefix can't have any spaces" 33 | 34 | - name: prefix-64-chars 35 | # 123456789 123456789 123456789 123456789 123456789 123456789 1234 36 | typeid: "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl_00000000000000000000000000" 37 | description: "The prefix can't be 64 characters, it needs to be 63 characters or less" 38 | 39 | - name: separator-empty-prefix 40 | typeid: "_00000000000000000000000000" 41 | description: "If the prefix is empty, the separator should not be there" 42 | 43 | - name: separator-empty 44 | typeid: "_" 45 | description: "A separator by itself should not be treated as the empty string" 46 | 47 | - name: suffix-short 48 | typeid: "prefix_1234567890123456789012345" 49 | description: "The suffix can't be 25 characters, it needs to be exactly 26 characters" 50 | 51 | - name: suffix-long 52 | typeid: "prefix_123456789012345678901234567" 53 | description: "The suffix can't be 27 characters, it needs to be exactly 26 characters" 54 | 55 | - name: suffix-spaces 56 | # This example has the right length, so that the failure is caused by the space 57 | # and not the suffix length 58 | typeid: "prefix_1234567890123456789012345 " 59 | description: "The suffix can't have any spaces" 60 | 61 | - name: suffix-uppercase 62 | # This example is picked because it would be valid in lowercase 63 | typeid: "prefix_0123456789ABCDEFGHJKMNPQRS" 64 | description: "The suffix should be lowercase with no uppercase letters" 65 | 66 | - name: suffix-hyphens 67 | # This example has the right length, so that the failure is caused by the hyphens 68 | # and not the suffix length 69 | typeid: "prefix_123456789-123456789-123456" 70 | description: "The suffix can't have any hyphens" 71 | 72 | - name: suffix-wrong-alphabet 73 | typeid: "prefix_ooooooiiiiiiuuuuuuulllllll" 74 | description: "The suffix should only have letters from the spec's alphabet" 75 | 76 | - name: suffix-ambiguous-crockford 77 | # This example would be valid if we were using the crockford disambiguation rules 78 | typeid: "prefix_i23456789ol23456789oi23456" 79 | description: "The suffix should not have any ambiguous characters from the crockford encoding" 80 | 81 | - name: suffix-hyphens-crockford 82 | # This example would be valid if we were using the crockford hyphenation rules 83 | typeid: "prefix_123456789-0123456789-0123456" 84 | description: "The suffix can't ignore hyphens as in the crockford encoding" 85 | 86 | - name: suffix-overflow 87 | # This is the first suffix that overflows into 129 bits 88 | typeid: "prefix_8zzzzzzzzzzzzzzzzzzzzzzzzz" 89 | description: "The suffix should encode at most 128-bits" 90 | 91 | # Tests below were added in v0.3.0 when we started allowing '_' within the 92 | # type prefix. 93 | - name: prefix-underscore-start 94 | typeid: "_prefix_00000000000000000000000000" 95 | description: "The prefix can't start with an underscore" 96 | 97 | - name: prefix-underscore-end 98 | typeid: "prefix__00000000000000000000000000" 99 | description: "The prefix can't end with an underscore" 100 | 101 | - name: empty 102 | typeid: "" 103 | description: "The empty string is not a valid typeid" 104 | 105 | - name: prefix-empty 106 | typeid: "prefix_" 107 | description: "The suffix can't be the empty string" 108 | -------------------------------------------------------------------------------- /spec/valid.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "nil", 4 | "typeid": "00000000000000000000000000", 5 | "prefix": "", 6 | "uuid": "00000000-0000-0000-0000-000000000000" 7 | }, 8 | { 9 | "name": "one", 10 | "typeid": "00000000000000000000000001", 11 | "prefix": "", 12 | "uuid": "00000000-0000-0000-0000-000000000001" 13 | }, 14 | { 15 | "name": "ten", 16 | "typeid": "0000000000000000000000000a", 17 | "prefix": "", 18 | "uuid": "00000000-0000-0000-0000-00000000000a" 19 | }, 20 | { 21 | "name": "sixteen", 22 | "typeid": "0000000000000000000000000g", 23 | "prefix": "", 24 | "uuid": "00000000-0000-0000-0000-000000000010" 25 | }, 26 | { 27 | "name": "thirty-two", 28 | "typeid": "00000000000000000000000010", 29 | "prefix": "", 30 | "uuid": "00000000-0000-0000-0000-000000000020" 31 | }, 32 | { 33 | "name": "max-valid", 34 | "typeid": "7zzzzzzzzzzzzzzzzzzzzzzzzz", 35 | "prefix": "", 36 | "uuid": "ffffffff-ffff-ffff-ffff-ffffffffffff" 37 | }, 38 | { 39 | "name": "valid-alphabet", 40 | "typeid": "prefix_0123456789abcdefghjkmnpqrs", 41 | "prefix": "prefix", 42 | "uuid": "0110c853-1d09-52d8-d73e-1194e95b5f19" 43 | }, 44 | { 45 | "name": "valid-uuidv7", 46 | "typeid": "prefix_01h455vb4pex5vsknk084sn02q", 47 | "prefix": "prefix", 48 | "uuid": "01890a5d-ac96-774b-bcce-b302099a8057" 49 | }, 50 | { 51 | "name": "prefix-underscore", 52 | "typeid": "pre_fix_00000000000000000000000000", 53 | "prefix": "pre_fix", 54 | "uuid": "00000000-0000-0000-0000-000000000000" 55 | } 56 | ] 57 | -------------------------------------------------------------------------------- /spec/valid.yml: -------------------------------------------------------------------------------- 1 | # This file contains test data that should parse as valid TypeIDs by conforming 2 | # implementations. 3 | # 4 | # Each example contains: 5 | # - The TypeID in its canonical string representation. 6 | # - The prefix 7 | # - The decoded UUID as a hex string 8 | # 9 | # Implementations should verify that they can encode/decode the data 10 | # in both directions: 11 | # 1. If the TypeID is decoded, it should result in the given prefix and UUID. 12 | # 2. If the UUID is encoded as a TypeID with the given prefix, it should 13 | # result in the given TypeID. 14 | # 15 | # In addition to using these examples, it's recommended that implementations 16 | # generate a thousands of random ids during testing, and verify that after 17 | # decoding and re-encoding the id, the result is the same as the original. 18 | # 19 | # In other words, the following property should always hold: 20 | # random_typeid == encode(decode(random_typeid)) 21 | # 22 | # Finally, while implementations should be able to decode the values below, 23 | # note that not all of them are UUIDv7s. When *generating* new random typeids, 24 | # implementations should always use UUIDv7s. 25 | # 26 | # Last updated: 2024-04-10 (for version 0.3.0 of the spec) 27 | 28 | - name: nil 29 | typeid: "00000000000000000000000000" 30 | prefix: "" 31 | uuid: "00000000-0000-0000-0000-000000000000" 32 | 33 | - name: one 34 | typeid: "00000000000000000000000001" 35 | prefix: "" 36 | uuid: "00000000-0000-0000-0000-000000000001" 37 | 38 | - name: ten 39 | typeid: "0000000000000000000000000a" 40 | prefix: "" 41 | uuid: "00000000-0000-0000-0000-00000000000a" 42 | 43 | - name: sixteen 44 | typeid: "0000000000000000000000000g" 45 | prefix: "" 46 | uuid: "00000000-0000-0000-0000-000000000010" 47 | 48 | - name: thirty-two 49 | typeid: "00000000000000000000000010" 50 | prefix: "" 51 | uuid: "00000000-0000-0000-0000-000000000020" 52 | 53 | - name: max-valid 54 | typeid: "7zzzzzzzzzzzzzzzzzzzzzzzzz" 55 | prefix: "" 56 | uuid: "ffffffff-ffff-ffff-ffff-ffffffffffff" 57 | 58 | - name: valid-alphabet 59 | typeid: "prefix_0123456789abcdefghjkmnpqrs" 60 | prefix: "prefix" 61 | uuid: "0110c853-1d09-52d8-d73e-1194e95b5f19" 62 | 63 | - name: valid-uuidv7 64 | typeid: "prefix_01h455vb4pex5vsknk084sn02q" 65 | prefix: "prefix" 66 | uuid: "01890a5d-ac96-774b-bcce-b302099a8057" 67 | 68 | # Tests below were added in v0.3.0 when we started allowing '_' within the 69 | # type prefix. 70 | - name: prefix-underscore 71 | typeid: "pre_fix_00000000000000000000000000" 72 | prefix: "pre_fix" 73 | uuid: "00000000-0000-0000-0000-000000000000" 74 | --------------------------------------------------------------------------------