TypeID in Lua

3 days ago 1

Published: 2025-05-21
Category: Code
Tags: Ansible Caddy Hatchbox  Lobsters Lua TypeID nginx

I’ve published a Lua implementation of TypeId:

https://github.com/pushcx/typeid-lua

TypeID is a nice standard for creating unique id tokens with a Stripe-like ⊕An aside to Stripe from an Xtripe: Please write a eng blog post about the features and history of tokens. It would be pillar content that would be enormously popular for meaningfully advancing the state of the art to a new standard. hungarian notation:

TypeIDs are a modern, type-safe extension of UUIDv7. Inspired by a similar use of prefixes in Stripe’s APIs.

TypeIDs are canonically encoded as lowercase strings consisting of three parts:

  1. A type prefix (at most 63 characters in all lowercase snake_case ASCII [a-z_]).
  2. An underscore ‘_’ separator
  3. A 128-bit UUIDv7 encoded as a 26-character string using a modified base32 encoding.

Here’s an example of a TypeID of type user:

user_2x4y6z8a0b1c2d3e4f5g6h7j8k └──┘ └────────────────────────┘ type uuid suffix (base32)

A formal specification defines the encoding in more detail.

Cleverly, the spec comes with a suite of labeled test cases of valid and invalid examples. I wish more specs did this!

I’m happy with the functionality my library offers, and there was the familiar delight of making things the first time I round-tripped a TypeID.

TypeID = require("typeid") -- or in dev: TypeID = require("./typeid") t = TypeID.generate("comment") -- t = { -- prefix = "comment", -- suffix = "01jvbhbbdje07rnyqkvstpvcge" -- } -- TypeID tables implement __tostring print(t) -- "comment_01jvbhbbdje07rnyqkvstpvcge" -- You can extract a standard UUID string t:uuid() -- "0196d715-adb2-700f-8afa-f3de756db20e" -- and round trip that back into a TypeID TypeID.from_uuid_string("comment", "0196d715-adb2-700f-8afa-f3de756db20e") -- parse and validate a TypeID from a string TypeID.parse("comment_01jvbhbbdje07rnyqkvstpvcge") -- finally, you can generate with a unix timestamp in ms: TypeID.generate("comment", 1) -- "comment_0000000001e8avt0nh7a68v2jc"

This was a fun practice project for me. I’ve used Lua more and more over the last few years in video game scripts and my window manager, and while 1-based array indexes will always feel odd, I think there’s a lot of potential in the language.

I experimented with style while implementing, and a lot of what I’m taking away from it is idioms I’m ignorant of. The TypeID is more OO style and returns an object with a method; the Base32 and UUID7 modules work on primitives. After implementing, I guess users would probably prefer getting a primitive back, as there doesn’t seem to be an idiomatic way to type-check. A module can export a trusted constructor, but without types there’s no way to use that to prevent instantiating invalid objects; everything is a table anyways. Coming from Ruby and ActiveRecord it’s frustrating to have most of a solution to the pervasive problem of passing around invalid objects but not be able to complete it.

I guess have to read popular libraries to get a feel for style. I don’t really know what level to aim at between “data-hiding high-level interface” and “yolo, all primitives and seams showing for perf”. Maybe it’s different inside and outside of games.

Along those lines, I ported Base32 from the official TypeID Golang implementation and then wrote UUID7 in bytes to match it. But all that intermediate bit twiddling could be simplified by generating a UUID7 directly into the Base32 encoding if I wanted to spend a lot more time on this.

Maybe I’m looking under the wrong name, but it seems odd there isn’t a bitfield type I could use, given Lua’s popularity in games. Some searching turned up a library but the absence of multi-bit operations seems inconvenient. Which points to:

-- typeid.lua uuid = function(self) return UUID7.to_string(Base32.decode(self.suffix)) end

There’s a code smell in TypeID: the nested conversions in the metatable uuid function suggest the internal representation of suffix is wrong. The syntactic distinction between .field and :method() means duplicating the data into two fields, exposing the internal representation by it being a field and the other a method, or getting away from what seems like common struct-y style and making both into methods. I’ve really grown to like the way Ruby’s optional parenthesis blur the line on fields and methods.

I wrote this library because I’d like to add a TypeID request identifier as a trace ID that nginx would generate and log, and pass along through Rails logs to MariaDB logs. It’s overkill for Lobsters but once a year I really wanted the ability to correlate logs like that. While the ROI may not really justify the time, it was a uniquely well-scoped small practice project.

Writing in Lua and adding the Lua module support to nginx seemed an easier path than writing in C and adding that compilation step to the deploy pipeline. Ultimately though, I’m not going to write that wrapper module.

On a parallel track, our ansible setup has slowly been succumbing to bit rot and my inexpert maintenance. I learned that Hatchbox could fill the same role and paying a couple bucks a month means it’s maintained by an expert professional. So 355e3b are going to move our hosting over soon, and it uses Caddy instead of nginx, so I guess I’ll wrap the official TypeID Golang implementation in a Caddy module instead. Still, it’s rewarding to contribute to TypeID’s list of supported languages.

Read Entire Article