k9

package
v0.85.0-pre.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 3, 2026 License: BSD-3-Clause Imports: 4 Imported by: 0

README

k9

import "github.com/vormadev/vorma/kit/k9"

Compact, k-sortable IDs with a human-friendly string form.

  • Binary ID size: 16 bytes (k9.IDSize)
  • String size: 26 chars (lowercase base32, no padding)

Layout:

  • High 52 bits: Unix timestamp in 100-microsecond ticks (big-endian)
  • Low 76 bits: cryptographically random entropy

Byte-level split:

  • Bytes 0..5 + high nibble of byte 6: 52-bit timestamp
  • Low nibble of byte 6 + bytes 7..15: 76-bit randomness

Why This Layout

This layout balances compactness, sortability, and readability:

  • Same binary payload size as UUIDv7 (16 bytes)
  • Shorter text form (26 chars vs UUID canonical 36)
  • More random entropy (76 bits vs UUIDv7 74)
  • Better time precision (100µs vs UUIDv7 1ms, 10x finer)
  • Longer timestamp horizon (year 16241 vs UUIDv7 year 10889, +5352 years)

Practical Benefits

  • K-sortable raw bytes improve index locality and make time-range scans/cursor pagination straightforward.
  • 26-char lowercase base32 strings are shorter and cleaner in logs/URLs than canonical UUID strings (36 chars with hyphens).
  • XOR mixing before encoding improves visual differentiation between nearby IDs without losing round-trip fidelity.

k9 vs UUIDv7

Property k9 UUIDv7
Binary size 16 bytes 16 bytes
Canonical string size 26 chars 36 chars (8-4-4-4-12 with hyphens)
Timestamp precision 100µs ticks (52-bit field) Milliseconds (48-bit field, RFC 9562)
Random entropy 76 bits 74 bits (rand_a + rand_b, RFC 9562)
String alphabet a-z2-7 (base32, no punctuation) Hex + hyphens
String sort == time sort No (by design, XOR-mixed prefix) Usually yes in canonical text form

Timestamp horizon (from Unix epoch):

  • k9: through 16241-05-04T13:38:57.0495Z (year 16241)
  • UUIDv7: through 10889-08-02T05:31:50.655Z

Example Strings

Sequential k9 strings from one run:

5cbjio47mq56tazuwwdfovdrpa
ksl2la6ljiyvlfqfbxjh44lp2e
qh3c2pxetnvib54nwd624tfo7y
orcvxlrpmughkrh3ea3fb4wggu
uo7flq3vpas2fp7vjvwe55rs2y

Sequential UUIDv7 canonical strings from one run:

019c3416-8ebf-77db-b919-cd0421bd52d7
019c3416-8ec0-7d75-bba7-8735991da697
019c3416-8ec2-73e8-9980-64965c007d23
019c3416-8ec3-7966-9258-0cbff8468bc5
019c3416-8ec4-7eec-a276-02f1ce6dd450

Important Ordering Note

For DB queries that require time ordering or range scans, store the raw 16-byte ID (BYTEA/BINARY(16) style), not the 26-char string.

The encoded string is for display, logs, URLs, and APIs; because of the XOR mixing step, lexical string order is not timestamp order.

Security

k9 uses cryptographically secure randomness for its random suffix, but it is an identifier format, not a secret/token format.

Safe/common uses:

  • Public object identifiers (users, orgs, orders, jobs, events)
  • IDs that should be hard to guess at scale

Not appropriate uses:

  • API keys, bearer tokens, password reset tokens, session secrets
  • Contexts that require opaque, non-structured secrets
  • Contexts where revealing creation-time information is unacceptable

Important:

  • The timestamp is encoded in the ID and is recoverable.
  • The XOR step is not encryption or obfuscation for security; it is only a human-friendliness transform to make nearby IDs easier to distinguish.

Secret token generation should use a separate high-entropy random value (typically 128+ random bits); k9 IDs should not be reused as secrets.

Basic Usage

id, err := k9.New()
if err != nil {
	return err
}

s := id.Serialize() // 26-char lowercase base32

parsed, err := k9.Parse(s)
if err != nil {
	return err
}

createdAt := parsed.CreationTime()
_ = createdAt

Batch creation with unified error handling:

ids, err := k9.NewMulti(3) // e.g. userID, orgID, inviteID
if err != nil {
	return err
}
userID := ids[0]
orgID := ids[1]
inviteID := ids[2]

IDs returned from a single k9.NewMulti call share the same timestamp.

Inclusive time-range query bounds:

start := time.Date(2026, 2, 6, 0, 0, 0, 0, time.UTC)
end := time.Date(2026, 2, 7, 0, 0, 0, 0, time.UTC)
startID := k9.MinIDAtTime(start)
endID := k9.MaxIDAtTime(end)
// WHERE id >= startID AND id <= endID

Constants

  • Exported constant: k9.IDSize (16).
  • k9.ID is a fixed-size [16]byte type.

Public API Summary

  • Generate IDs: k9.New(), k9.NewMulti(n).
  • Encode/decode text: k9.Serialize(id), id.Serialize(), k9.Parse(str).
  • Convert raw bytes: k9.FromBytes(raw).
  • Read time metadata: k9.ToUnixMicro(id), id.ToUnixMicro(), k9.CreationTime(id), id.CreationTime().
  • Build range bounds for scans: k9.MinIDAtTime(t), k9.MaxIDAtTime(t).
  • Validation errors: k9.ErrInvalidCount, k9.ErrInvalidTextLength, k9.ErrInvalidIDLength.

Validation Behavior

  • k9.Parse(str) requires exactly 26 chars, otherwise returns k9.ErrInvalidTextLength.
  • k9.FromBytes(raw) requires exactly k9.IDSize bytes, otherwise returns k9.ErrInvalidIDLength.
  • k9 timestamps are stored at 100µs resolution (ToUnixMicro always returns a multiple of 100).

Why Is It Called k9?

Because the byte representation is k-sortable, and it contains over 9 bytes of entropy (9.5, to be exact).

Documentation

Overview

Package k9 generates 16-byte k-sortable IDs, with human-friendly string encoding. Leading 52 bits are a Unix timestamp in 100-microsecond ticks (big endian). Trailing 76 bits are cryptographically random. String representation is 26 characters (base32-encoded, with no padding, and with bytes 0-6 XOR'd with bytes 7-13, respectively, to make it easier for human eyes to differentiate IDs). Only the byte representation is k-sortable, not the string representation.

Index

Constants

View Source
const (
	// IDSize is the size, in bytes, of a k9 ID.
	IDSize = 16
)

Variables

View Source
var (

	// ErrInvalidCount indicates an invalid count argument.
	ErrInvalidCount = errors.New("k9: invalid count")
	// ErrInvalidIDLength indicates an invalid raw ID byte length.
	ErrInvalidIDLength = errors.New("k9: invalid ID length")
	// ErrInvalidTextLength indicates an invalid encoded ID text length.
	ErrInvalidTextLength = errors.New("k9: invalid encoded ID length")
)

Functions

func CreationTime

func CreationTime(id ID) time.Time

CreationTime converts a k9 ID to a time.Time object representing the creation time in microseconds.

func Serialize

func Serialize(id ID) string

Serialize encodes a k9 ID into a 26-character, human-friendly string representation.

func ToUnixMicro

func ToUnixMicro(id ID) int64

ToUnixMicro converts a k9 ID to a Unix timestamp in microseconds.

Types

type ID

type ID [IDSize]byte

ID is a 16-byte k9 ID.

func FromBytes

func FromBytes(bytes []byte) (ID, error)

FromBytes validates and converts raw 16-byte input into a k9 ID.

func MaxIDAtTime

func MaxIDAtTime(t time.Time) ID

MaxIDAtTime returns the latest possible ID for the given time.

func MinIDAtTime

func MinIDAtTime(t time.Time) ID

MinIDAtTime returns the earliest possible ID for the given time.

func New

func New() (ID, error)

New generates a new 16-byte k9 ID, beginning with the unix timestamp at creation (in 100-microsecond ticks, big-endian), followed by 76 bits of cryptographic randomness.

func NewMulti

func NewMulti(n int) ([]ID, error)

NewMulti generates n new IDs with unified error handling. Returns ErrInvalidCount when n is negative or too large. IDs produced by a single call share one timestamp tick.

func Parse

func Parse(str string) (ID, error)

Parse decodes a k9 ID string back into its byte representation.

func (ID) CreationTime

func (id ID) CreationTime() time.Time

CreationTime converts a k9 ID to a time.Time object representing the creation time in microseconds.

func (ID) Serialize

func (id ID) Serialize() string

Serialize encodes a k9 ID into a 26-character, human-friendly string representation.

func (ID) ToUnixMicro

func (id ID) ToUnixMicro() int64

ToUnixMicro converts a k9 ID to a Unix timestamp in microseconds.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL