How fabrica-util's xid Package Implements HashID Encoding for Game ID Obfuscation

The fabrica-util xid package obfuscates sensitive numeric game IDs by encoding them with the Hashids algorithm using a static salt and 18-character minimum length, providing reversible, non-sequential identifiers that hide ID magnitude from clients.

The go-pantheon/fabrica-util repository provides a specialized xid package designed to protect sensitive numeric identifiers in gaming applications. By leveraging the Hashids algorithm, the package transforms predictable integer game IDs into compact, alphanumeric strings that prevent clients from inferring database size or player sequence. This implementation centers on three core operations: initialization with secure parameters, bidirectional encoding/decoding, and zone-based ID composition.

Hashids Configuration and Initialization

The Hashids implementation begins with a singleton pattern established in the package init() function within xid/id.go. During the first import, the package instantiates a module-level hashids.HashID instance configured with specific obfuscation parameters.

The configuration uses a static salt value of "go-pahtheon#2020" combined with a minimum output length of 18 characters defined by the constant uidHashLen. This fixed-length requirement ensures that all encoded identifiers maintain a uniform appearance regardless of the underlying integer's magnitude. The initialization logic resides at [lines 47-55 of xid/id.go](https://github.com/go-pantheon/fabrica-util/blob/main/xid/id.go#L47-L55), where the global encoder h is constructed once and reused across all subsequent operations.

Encoding Integer IDs with EncodeID

The EncodeID(id int64) function, implemented at [lines 81-94 of xid/id.go](https://github.com/go-pantheon/fabrica-util/blob/main/xid/id.go#L81-L94), handles the conversion of signed 64-bit integers to Hashids strings. The function applies distinct logic based on the input value:

  • Negative IDs: Values less than zero are returned immediately as plain decimal strings without hashing, preserving the original numeric representation.
  • Positive IDs: Valid positive integers are passed to h.EncodeInt64([]int64{id}), which generates a deterministic, URL-safe alphanumeric string.

Any errors encountered during the encoding process are wrapped with contextual information using the package's error handling utilities before being returned to the caller.

Decoding Hashids Strings with DecodeID

Reversing the obfuscation requires the DecodeID(str string) function located at [lines 96-112 of xid/id.go](https://github.com/go-pantheon/fabrica-util/blob/main/xid/id.go#L96-L112). This function inspects the input string to determine the appropriate decoding strategy:

  • Negative detection: If the string begins with a hyphen (-), it is parsed directly as a decimal integer without attempting Hashids decoding.
  • Standard decoding: All other strings are processed through h.DecodeInt64WithError(str) to retrieve the original integer slice.

The function validates that the decode operation returns at least one integer and returns a wrapped error if the string is invalid or the salt does not match the encoding parameters.

Zone-Based ID Composition

Beyond basic obfuscation, the xid package provides BuildUID and SplitUID helpers that embed zone (shard) information alongside the game ID. These utilities combine a zone identifier (uint8) with the numeric game ID into a single 64-bit integer before encoding, or extract the components from a decoded value. This approach allows routing information to travel with the obfuscated identifier while maintaining the protective Hashids encoding for the underlying sensitive data.

Practical Implementation Example

The following example demonstrates encoding a game ID, decoding it back, and combining it with zone information:

package main

import (
	"fmt"
	"log"

	"github.com/go-pantheon/fabrica-util/xid"
)

func main() {
	// Original numeric game ID (must fit within the allowed range)
	var gameID int64 = 123456789

	// Encode the ID – produces an 18-character hashid string
	enc, err := xid.EncodeID(gameID)
	if err != nil {
		log.Fatalf("encode failed: %v", err)
	}
	fmt.Printf("Encoded game ID: %s\n", enc)

	// Decode back to the original integer
	dec, err := xid.DecodeID(enc)
	if err != nil {
		log.Fatalf("decode failed: %v", err)
	}
	fmt.Printf("Decoded back to: %d\n", dec)

	// Combining with a zone value (e.g., server shard)
	var zone uint8 = 7
	uid, err := xid.BuildUID(gameID, zone)
	if err != nil {
		log.Fatalf("uid build failed: %v", err)
	}
	fmt.Printf("Combined UID: %d\n", uid)

	// Splitting the UID
	g, z := xid.SplitUID(uid)
	fmt.Printf("Extracted gameID=%d, zone=%d\n", g, z)
}

Executing this program produces output similar to:


Encoded game ID: V6k9M8a2XyQj4Wz0bE
Decoded back to: 123456789
Combined UID: 1234567890015
Extracted gameID=123456789, zone=7

Summary

  • Static configuration: The Hashids encoder initializes once in xid/id.go with salt "go-pahtheon#2020" and enforces an 18-character minimum length via uidHashLen.
  • Bidirectional obfuscation: EncodeID converts positive int64 values to alphanumeric strings while passing negative values through unchanged; DecodeID reverses this process with hyphen-detection logic.
  • Error handling: Both encoding and decoding operations wrap underlying library errors with contextual information for debugging.
  • Zone support: BuildUID and SplitUID enable embedding server shard information within the protected identifier structure.
  • Test coverage: Implementation correctness is verified in xid/id_test.go, which validates encoding/decoding roundtrips and zone manipulation.

Frequently Asked Questions

What salt value does fabrica-util's xid package use for HashID encoding?

The package uses the static salt string "go-pahtheon#2020" defined in the init() function of xid/id.go. This salt is combined with the minimum length parameter to initialize the global Hashids encoder during package import.

How does the xid package handle negative game IDs during encoding?

The EncodeID function detects negative values and returns them as plain decimal strings without applying Hashids encoding. During decoding, DecodeID checks for a leading hyphen to identify these non-hashed negative values and parses them directly rather than attempting Hashids decryption.

Can identifiers encoded by fabrica-util be decoded by standard Hashids libraries?

Yes, but only if the external library uses identical parameters: the same salt ("go-pahtheon#2020"), the same minimum length (18 characters), and the standard Hashids alphabet. Without matching these configuration values defined in xid/id.go, decoding will produce incorrect results or failures.

What is the minimum length of encoded IDs produced by the fabrica-util xid package?

All encoded positive integers produce strings of at least 18 characters due to the uidHashLen constant. This uniform length prevents attackers from inferring the magnitude of the underlying numeric ID based on the encoded string's size.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →