UUIDs are often generated client-side. Am I right in thinking that this isn’t possible with this approach? Even if you let clients give you UUIDs and they gave them back the masked versions, wouldn't you be vulnerable to a client providing two UUIDs with different ts and the same rand? So this is only designed for when you are generating the UUIDv7s yourself?
Of course, UUIDv4 on the client side is not without risk either- needing to validate uniqueness and not re-use of some other ID. For the UUIDv7 on client side- you could add some sanity validation- but really I think it’s best avoided.
creating them server-side risks having a network error cause a client to have requested a resource be created without receiving its id due to a network error before receiving the response, risking double submissions and generally bad recovery options from the UI.
if you need users to provide uuids for consistent network operations, you can have an endpoint responsible for generating signed uuids that expire after a short interval, thereby controlling uuid-time drift (must be used within 1-5 minutes, perhaps), ensuring the client can't forge them to mess with your backend, and still provide a nice and stable client-side-uuid system.
for the uuidv47 thing, you would apply their XOR trick prior to sending the UUID to the user. you presumably just reverse the XOR trick to get the UUIDv7 back from the UUIDv4 you passed them.
since when?
Of course, adding two IDs for a resource complicates things. But so too does trusting client-generated IDs to be universally unique.
1. You implicitly take away someone else's hypothetical benefit of leveraging UUID v7, which is disappointing for any consumer of your API.
2. By storing the UUIDs differently on your API service from internally, you're going to make your life just a tiny bit harder because now you have to go through this indirection of conversion, and I'm not sure if this is worth it.
With a sufficient number of users of an API,
it does not matter what you promise in the contract:
all observable behaviors of your system
will be depended on by somebody.
* https://www.hyrumslaw.comUsually if you see an id in your http logs you can simply search your database for that id. The v4 to v7 indirection creates a small inconvenience.
The mismatch may be resolved if this was available as a fully transparent database optimization.
1. Not leaking timestamp data (security/regulations)
2. Having easily time-sortable primary keys (DB performance/etc.)
If you don't have both of these needs, the tool is an unnecessary indirection, as you've identified in (2).
However, where you do have both needs, some indirection is necessary. Whether this is the correct one is a different question.
Similarly, if you _must not_ leak timestamps for some real-world reason, (1) is an intrinsic requirement, consumers be damned.
Also, why use UUIDs in that case?
So it is not generally fit for that purpose either.
So there's definitely some gotchas with relying on rand_a and rand_b in UUIDv7 for seeding a PRF, and when ingesting data from devices outside of your trust boundary (as may be the case with high-volume telemetry), even if you wrote the code they basically can't be trusted for this purpose, and if those bits are undisturbed in the output it's certainly a problem if the idea was to obfuscate serialisation, timing, or correlation.
Even generations we might assume are safe may not be completely safe; for example, the new uuidv7() in PostgreSQL 18 fills rand_a entirely from the high precision part of the timestamp, and this is RFC compliant. So if an import routine generates a big batch of such UUIDs, this v7-to-v4 scheme discloses output bits that can be used to relate individual records as part of the same group. That might be fine for data points pertaining to a vehicle engine. It might not be fine for identifiers that relate to people.
So, since not all UUIDv7 is created alike, I'd add a strong caveat: unless generating the rand_a and rand_b bits entirely oneself with a high degree of confidence in their nonguessibility, then this scheme may still leak information regarding timing, sequence, or correlation of records, and you will have to read the source code of your UUIDv7 implementation to know for sure.
If they are suitably random then this scheme seems to check out, but you're going to need some barbed wire and some inspiration from these https://en.wikipedia.org/wiki/Long-term_nuclear_waste_warnin... on anything that can generate v7 IDs.
How it works: the 48-bit timestamp is XOR-masked with a keyed SipHash-2-4 stream derived from the UUID’s random field. The random bits are preserved, the version flips between 7 (inside) and 4 (outside), and the RFC variant is kept. The mapping is injective: (ts, rand) → (encTS, rand). Decode is just encTS ⊕ mask, so round-trip is exact.
Security: SipHash is a PRF, so observing façades doesn’t leak the key. Wrong key = wrong timestamp. Rotation can be done with a key-ID outside the UUID.
Performance: one SipHash over 10 bytes + a couple of 48-bit loads/stores. Nanosecond overhead, header-only C11, no deps, allocation-free.
Tests: SipHash reference vectors, round-trip encode/decode, and version/variant invariants.
Curious to hear feedback!