UUIDv47: Store UUIDv7 in DB, emit UUIDv4 outside (SipHash-masked timestamp)

github.com

142 points by aabbdev 11 hours ago


chrismorgan - 9 hours ago

A few years ago I made a scheme whereby you could use sequential numeric IDs in your database, but expose them as short random strings (length 4–20 step 2, depending on numeric value and sparsity configuration). It used some custom instances of the Speck cipher family, and I think it’s robust and rather neat.

Although I finished it, I never quite published it properly for some reason, probably partly because I shelved the projects where I had been going to use it (I might unshelve one of them next year).

Well, I might as well share it, because it’s quite relevant here and interesting:

https://temp.chrismorgan.info/2025-09-17-tesid/

My notes on its construction, pros and cons are fairly detailed.

Maybe I’ll go back and publish it properly next year.

aabbdev - 11 hours ago

Hi, I’m the author of uuidv47. The idea is simple: keep UUIDv7 internally for database indexing and sortability, but emit UUIDv4-looking façades externally so clients don’t see timing patterns.

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!

chuckadams - 8 hours ago

I remember doing something similar, but I just used two columns, a public uuid, and a bigint primary key that wasn't exposed to the api (this was long before uuidv7). Lacked a lot of the conveniences of using uuid everywhere, but it still handled the use case of merging different DB dumps as long as PKs were stripped out first.

And maybe I misunderstand how the hashing works, but it seems if you're looking things up by the hashed uuid, you're still going to want two columns anyway.

sgarland - an hour ago

This is cool, but the entire “OMG you can’t leak timestamps” has always reeked of security theater to me, as has the argument that if you expose sequential IDs, you’re opening vectors of attack, exposing business information, etc.

Add some random large value to your ints periodically - they’ll still be monotonic, but you’ll throw off the dastardly spies stealing your super valuable business intelligence.

miningape - 9 hours ago

This is interesting, but is almost something I'd rather have the DB handle for me - i.e. I can cast a UUIDv7 to "UUIDv4" (and vice versa) and I could use both in queries (with explicit syntax to annotate which kind is being used / expected)

tracker1 - 9 hours ago

Interesting project... just out of curiosity, could you give something resembling a couple practical examples of the risk of exposing the time portion of a v7 UUID?

bismark - 9 hours ago

My biggest issue w/ UUIDv7 is how challenging they are to visually diff when looking at a list. Having some sort of visual translation layer in psql that would render them with the random bits first while maintaining the time sorting underneath would be a major UX boost...

g-mork - 9 hours ago

Vaguely related technique with similar goals (but I love the one posted here) http://blog.notdot.net/2007/9/Damn-Cool-Algorithms-Part-2-Se...

taminka - 8 hours ago

i'm curious, if you're doing single header, why not also do the stb-style IMPL block + definitions block such that you avoid the issues from accidentally including the header multiple times?

LeicaLatte - 8 hours ago

Mobile apps often sort by creation time in the UI (chat messages, activity feeds). Since clients only see the masked version, there might be a need to expose a separate timestamp field.

gwbas1c - 5 hours ago

I started encrypting database IDs and deriving GUIDs from that.

salterdavid032 - 6 hours ago

[dead]

jppope - 7 hours ago

Sounds like its trying to achieve something similar to what ULID is going for: https://github.com/ulid/spec

timestamp + readability

themafia - 6 hours ago

Why not just use UUIDv8? The format allows you to use the upper bits for a timestamp and the lower bits for any value you like, including just a random value.