⚡ UUIDv47 = v4 privacy + v7 performance
See the codeuuidv47 lets you store sortable UUIDv7 in your database while emitting a
UUIDv4‑looking façade at your API boundary. It XOR‑masks only the UUIDv7
timestamp field with a keyed SipHash‑2‑4 stream derived from the UUID’s own
random bits. The mapping is deterministic and exactly invertible.
#include <stdio.h>
#include "uuidv47.h"
int main(void){
const char* s = "00000000-0000-7000-8000-000000000000";
uuid128_t v7;
if (!uuid_parse(s, &v7)) return 1;
uuidv47_key_t key = { .k0 = 0x0123456789abcdefULL, .k1 = 0xfedcba9876543210ULL };
uuid128_t facade = uuidv47_encode_v4facade(v7, key);
uuid128_t back = uuidv47_decode_v4facade(facade, key);
char a[37], b[37], c[37];
uuid_format(&v7, a);
uuid_format(&facade, b);
uuid_format(&back, c);
printf("v7 (DB) : %s\n", a);
printf("v4 (API): %s\n", b);
printf("back : %s\n", c);
}
Build & run with the provided Makefile:
make test
make coverage
sudo make install # installs header into $(PREFIX)/include
typedef struct { uint8_t b[16]; } uuid128_t;
typedef struct { uint64_t k0, k1; } uuidv47_key_t;
uuid128_t uuidv47_encode_v4facade(uuid128_t v7, uuidv47_key_t key);
uuid128_t uuidv47_decode_v4facade(uuid128_t v4_facade, uuidv47_key_t key);
int uuid_version(const uuid128_t* u);
void set_version(uuid128_t* u, int ver);
void set_variant_rfc4122(uuid128_t* u);
bool uuid_parse (const char* str, uuid128_t* out);
void uuid_format(const uuid128_t* u, char out[37]);
ts48 ^ mask48(R), then set version = 4encTS ^ mask48(R), then set version = 710 bytes derived from the v7 random field:
msg[0] = (byte6 & 0x0F)
msg[1] = byte7
msg[2] = (byte8 & 0x3F)
msg[3..9] = bytes9..15
The mask is XOR with a keyed PRF → perfectly invertible when the key is known.
Mapping is injective; collisions reduce to duplicate randoms within the same ms.
If two IDs have the same randoms, but different timestamps, then XORing the facade IDs will reveal the XOR of the timestamps, because the mask will cancel out. This does not reveal the key, only an upper bound on the duration between when the IDs were generated. For a 1% chance of finding 2 matching randoms, using the birthday problem approximation, ~20 billion IDs would need to be generated, or ~60 billion for a 10% chance. For an attacker trying to reveal information about a chosen target ID, they would need to generate on average 2^74 IDs, which is infeasible.
make test
make coverage
make debug
sudo make install
# optional microbench
make bench && ./bench
This repo includes an optional Postgres extension that defines a uuid47 base
type, casts to/from core uuid, operators, B‑tree/hash opclasses, and a BRIN
(minmax‑multi) distance support function.
# From repo root (uses PGXS via pg_config)
make pginstall
Enable in a database:
CREATE EXTENSION uuid47; -- installs type and functions
-- (If you installed into a custom schema, add it to search_path.)
Run the SQL test suite end‑to‑end:
make pgtest PG_CONFIG=/opt/homebrew/opt/postgresql@17/bin/pg_config PSQL="/opt/homebrew/opt/postgresql@17/bin/psql" DBNAME=postgres
uuid47_generate() into a uuid47
column. Inserting into uuid triggers an assignment cast every row and can be
~2–3 µs/row slower.uuid47 uses ALIGNMENT = int4 (like core uuid) for
better tuple formation speed.SET uuid47.key = '0011223344556677:8899aabbccddeeff';
Parse happens once via a GUC assign hook and is cached per backend.Frontend/client-facing entities
→ Use UUIDv47 with a B-Tree index. Users aren’t expected to persist this ID and can tolerate cache resets. Secure the secret with an HSM and inject it safely into the process.
External service–facing entities
→ If the service is secure (e.g., financial), provide UUID7 (UUIDv47).
→ If the service is not secure, provide a secondary ID of type UUIDv4 with a hashmap index.
If the master key leaks, it’s almost certain your consumer data and systems have leaked as well—which is ultimately a legal problem, not a technical one. Data leaks will cause far greater issues than the compromise of an ID master key, which can be rotated safely since only the frontend depends on it.
Command: ./bench (2,000,000 iters, 1 warmup + 3 rounds)
Example (Apple M‑series):
iters=2000000, warmup=1, rounds=3
[warmup] 34.89 ns/op
[encode+decode] round 1: 33.80 ns/op, 29.6 Mops/s
[encode+decode] round 2: 38.16 ns/op, 26.2 Mops/s
[encode+decode] round 3: 33.33 ns/op, 30.0 Mops/s
[warmup] 14.83 ns/op
[siphash(10B)] round 1: 14.88 ns/op, 67.2 Mops/s
[siphash(10B)] round 2: 15.45 ns/op, 64.7 Mops/s
[siphash(10B)] round 3: 15.00 ns/op, 66.7 Mops/s
== best results ==
encode+decode : 33.00 ns/op (30.3 Mops/s)
siphash(10B) : 14.00 ns/op (71.4 Mops/s)
What it measures
encode+decode: full v7 → façade → v7 round‑trip.siphash(10B): SipHash‑2‑4 on the 10‑byte mask message.Build with
-O3 -march=nativefor best results.
Q: Why not xxHash with a secret?
A: Not a PRF; secret can leak. Use SipHash.
Q: Is the façade indistinguishable from v4?
A: Version/variant bits are v4; variable bits are uniformly distributed under the PRF.
MIT, Copyright (c) 2025 Stateless Limited
(top 24 of 44)
1,520 followers · starred Sep 2025
11,583 followers · starred Oct 2025
143 followers · starred Sep 2025
198 followers · starred Oct 2025
⚡ UUIDv47 = v4 privacy + v7 performance
See the codeuuidv47 lets you store sortable UUIDv7 in your database while emitting a
UUIDv4‑looking façade at your API boundary. It XOR‑masks only the UUIDv7
timestamp field with a keyed SipHash‑2‑4 stream derived from the UUID’s own
random bits. The mapping is deterministic and exactly invertible.
#include <stdio.h>
#include "uuidv47.h"
int main(void){
const char* s = "00000000-0000-7000-8000-000000000000";
uuid128_t v7;
if (!uuid_parse(s, &v7)) return 1;
uuidv47_key_t key = { .k0 = 0x0123456789abcdefULL, .k1 = 0xfedcba9876543210ULL };
uuid128_t facade = uuidv47_encode_v4facade(v7, key);
uuid128_t back = uuidv47_decode_v4facade(facade, key);
char a[37], b[37], c[37];
uuid_format(&v7, a);
uuid_format(&facade, b);
uuid_format(&back, c);
printf("v7 (DB) : %s\n", a);
printf("v4 (API): %s\n", b);
printf("back : %s\n", c);
}
Build & run with the provided Makefile:
make test
make coverage
sudo make install # installs header into $(PREFIX)/include
typedef struct { uint8_t b[16]; } uuid128_t;
typedef struct { uint64_t k0, k1; } uuidv47_key_t;
uuid128_t uuidv47_encode_v4facade(uuid128_t v7, uuidv47_key_t key);
uuid128_t uuidv47_decode_v4facade(uuid128_t v4_facade, uuidv47_key_t key);
int uuid_version(const uuid128_t* u);
void set_version(uuid128_t* u, int ver);
void set_variant_rfc4122(uuid128_t* u);
bool uuid_parse (const char* str, uuid128_t* out);
void uuid_format(const uuid128_t* u, char out[37]);
ts48 ^ mask48(R), then set version = 4encTS ^ mask48(R), then set version = 710 bytes derived from the v7 random field:
msg[0] = (byte6 & 0x0F)
msg[1] = byte7
msg[2] = (byte8 & 0x3F)
msg[3..9] = bytes9..15
The mask is XOR with a keyed PRF → perfectly invertible when the key is known.
Mapping is injective; collisions reduce to duplicate randoms within the same ms.
If two IDs have the same randoms, but different timestamps, then XORing the facade IDs will reveal the XOR of the timestamps, because the mask will cancel out. This does not reveal the key, only an upper bound on the duration between when the IDs were generated. For a 1% chance of finding 2 matching randoms, using the birthday problem approximation, ~20 billion IDs would need to be generated, or ~60 billion for a 10% chance. For an attacker trying to reveal information about a chosen target ID, they would need to generate on average 2^74 IDs, which is infeasible.
make test
make coverage
make debug
sudo make install
# optional microbench
make bench && ./bench
This repo includes an optional Postgres extension that defines a uuid47 base
type, casts to/from core uuid, operators, B‑tree/hash opclasses, and a BRIN
(minmax‑multi) distance support function.
# From repo root (uses PGXS via pg_config)
make pginstall
Enable in a database:
CREATE EXTENSION uuid47; -- installs type and functions
-- (If you installed into a custom schema, add it to search_path.)
Run the SQL test suite end‑to‑end:
make pgtest PG_CONFIG=/opt/homebrew/opt/postgresql@17/bin/pg_config PSQL="/opt/homebrew/opt/postgresql@17/bin/psql" DBNAME=postgres
uuid47_generate() into a uuid47
column. Inserting into uuid triggers an assignment cast every row and can be
~2–3 µs/row slower.uuid47 uses ALIGNMENT = int4 (like core uuid) for
better tuple formation speed.SET uuid47.key = '0011223344556677:8899aabbccddeeff';
Parse happens once via a GUC assign hook and is cached per backend.Frontend/client-facing entities
→ Use UUIDv47 with a B-Tree index. Users aren’t expected to persist this ID and can tolerate cache resets. Secure the secret with an HSM and inject it safely into the process.
External service–facing entities
→ If the service is secure (e.g., financial), provide UUID7 (UUIDv47).
→ If the service is not secure, provide a secondary ID of type UUIDv4 with a hashmap index.
If the master key leaks, it’s almost certain your consumer data and systems have leaked as well—which is ultimately a legal problem, not a technical one. Data leaks will cause far greater issues than the compromise of an ID master key, which can be rotated safely since only the frontend depends on it.
Command: ./bench (2,000,000 iters, 1 warmup + 3 rounds)
Example (Apple M‑series):
iters=2000000, warmup=1, rounds=3
[warmup] 34.89 ns/op
[encode+decode] round 1: 33.80 ns/op, 29.6 Mops/s
[encode+decode] round 2: 38.16 ns/op, 26.2 Mops/s
[encode+decode] round 3: 33.33 ns/op, 30.0 Mops/s
[warmup] 14.83 ns/op
[siphash(10B)] round 1: 14.88 ns/op, 67.2 Mops/s
[siphash(10B)] round 2: 15.45 ns/op, 64.7 Mops/s
[siphash(10B)] round 3: 15.00 ns/op, 66.7 Mops/s
== best results ==
encode+decode : 33.00 ns/op (30.3 Mops/s)
siphash(10B) : 14.00 ns/op (71.4 Mops/s)
What it measures
encode+decode: full v7 → façade → v7 round‑trip.siphash(10B): SipHash‑2‑4 on the 10‑byte mask message.Build with
-O3 -march=nativefor best results.
Q: Why not xxHash with a secret?
A: Not a PRF; secret can leak. Use SipHash.
Q: Is the façade indistinguishable from v4?
A: Version/variant bits are v4; variable bits are uniformly distributed under the PRF.
MIT, Copyright (c) 2025 Stateless Limited
(top 24 of 44)
1,520 followers · starred Sep 2025
11,583 followers · starred Oct 2025
143 followers · starred Sep 2025
198 followers · starred Oct 2025