Skip to main content

Overview

Osmium Chat Protocol uses Snowflake IDs for all primary identifiers. Snowflake IDs are 64-bit integers that encode both a timestamp and unique sequence, making them:
  • Sortable by creation time - Higher IDs are always newer
  • Globally unique - No collisions across distributed systems
  • Compact - Only 8 bytes, efficient for storage and transmission
  • Decentralized - Can be generated without coordination

The @snowflake Annotation

Throughout the protocol buffer definitions, you’ll see fields annotated with @snowflake<Type> in comments:
message User {
  // @snowflake<User>
  fixed64 id = 1;
  string name = 2;
  optional string username = 3;
}

message Message {
  refs.ChatRef chat_ref = 1;
  // @snowflake<Message>
  fixed64 message_id = 2;
  // @snowflake<User>
  fixed64 author_id = 3;
  string message = 4;
  // @snowflake<Message>
  optional fixed64 reply_to = 5;
}
This annotation indicates:
  1. The field is a Snowflake ID (not just any 64-bit integer)
  2. What type of entity the ID references
  3. The field should be treated as an opaque identifier

Snowflake Types

Snowflake IDs are used for all major entity types:
  • @snowflake<User> - User accounts
  • @snowflake<Session> - Authentication sessions
  • @snowflake<Message> - Individual messages
  • @snowflake<Chat> - Direct messages and group chats
  • @snowflake<Community> - Community/server instances
  • @snowflake<Channel> - Channels within communities
  • @snowflake<Role> - Permission roles
  • @snowflake<File> - Uploaded files and media
  • @snowflake<Emoji> - Custom emoji
  • @snowflake<StickerPack> - Sticker pack collections

Protocol Buffer Representation

In Protocol Buffers, Snowflake IDs use the fixed64 type:
message Channel {
  // @snowflake<Channel>
  fixed64 id = 1;
  // @snowflake<Community>
  fixed64 community_id = 2;
  string name = 3;
  // @snowflake<Channel>
  optional fixed64 parent_id = 6;
}
fixed64 is always 8 bytes on the wire, unlike uint64 which uses variable-length encoding. This is perfect for Snowflake IDs since they’re almost always large numbers that would take 8-10 bytes with variable encoding anyway.

Working with Snowflake IDs

JavaScript/TypeScript

// ⚠️ WRONG: JavaScript numbers lose precision above 2^53
const userId: number = 1234567890123456789; // Will be rounded!

// ✅ CORRECT: Use BigInt for Snowflake IDs
const userId: bigint = 1234567890123456789n;

// Converting from various formats
const fromString = BigInt('1234567890123456789');
const fromHex = BigInt('0x112210F47DE98115');

// Comparing Snowflakes (newer messages have higher IDs)
if (message1.message_id > message2.message_id) {
  console.log('message1 is newer');
}

// Sorting by creation time
messages.sort((a, b) => {
  if (a.message_id < b.message_id) return -1;
  if (a.message_id > b.message_id) return 1;
  return 0;
});
Never use JavaScript number type for Snowflake IDs. JavaScript numbers are IEEE 754 double-precision floats and can only safely represent integers up to 2^53 - 1. Snowflake IDs are 64-bit integers and will lose precision. Always use BigInt.

Python

# Python integers have arbitrary precision - no special handling needed!
user_id: int = 1234567890123456789

# Comparing and sorting works as expected
if message1.message_id > message2.message_id:
    print('message1 is newer')

messages.sort(key=lambda m: m.message_id)

# Converting to/from strings
user_id_str = str(user_id)
user_id = int(user_id_str)

Rust

// Use u64 for Snowflake IDs
let user_id: u64 = 1234567890123456789;

// Comparison and sorting work naturally
if message1.message_id > message2.message_id {
    println!("message1 is newer");
}

messages.sort_by_key(|m| m.message_id);

// Parsing from strings
let user_id: u64 = "1234567890123456789".parse()?;

Go

// Use uint64 for Snowflake IDs
var userID uint64 = 1234567890123456789

// Comparison works as expected
if message1.MessageID > message2.MessageID {
    fmt.Println("message1 is newer")
}

// Sorting
sort.Slice(messages, func(i, j int) bool {
    return messages[i].MessageID < messages[j].MessageID
})

// Parsing from strings
userID, err := strconv.ParseUint("1234567890123456789", 10, 64)

Snowflake ID Structure

While Snowflake IDs should generally be treated as opaque identifiers, understanding their structure can be useful:
 64                                                            0
  ┌─────────────┬──────────┬──────────┬──────────────────────┐
  │ Timestamp   │ Worker   │ Process  │ Sequence             │
  │ 42 bits     │ 5 bits   │ 5 bits   │ 12 bits              │
  └─────────────┴──────────┴──────────┴──────────────────────┘
Timestamp
42 bits
Milliseconds since custom epoch. This makes IDs sortable by creation time.
Worker ID
5 bits
Identifies which worker/datacenter generated the ID (0-31).
Process ID
5 bits
Identifies which process within the worker generated the ID (0-31).
Sequence
12 bits
Sequence number for IDs generated in the same millisecond (0-4095).

Extracting Timestamp (Advanced)

function getSnowflakeTimestamp(snowflake: bigint): Date {
  // Custom epoch (adjust based on actual implementation)
  const EPOCH = 1640995200000n; // Example: Jan 1, 2022
  
  // Extract timestamp (upper 42 bits)
  const timestamp = snowflake >> 22n;
  
  // Add epoch to get Unix timestamp
  const unixMs = timestamp + EPOCH;
  
  return new Date(Number(unixMs));
}

const messageId = 1234567890123456789n;
const createdAt = getSnowflakeTimestamp(messageId);
console.log('Message created at:', createdAt);
While you can extract timestamps from Snowflake IDs, don’t rely on this for business logic. The server may return explicit timestamps in message objects. Treat Snowflake IDs primarily as unique identifiers.

Common Usage Patterns

Pagination with Snowflakes

Snowflake IDs are perfect for cursor-based pagination:
// Fetch older messages (before a certain ID)
const response = await client.sendRequest({
  messages_get_history: {
    chat_ref: { channel: { community_id: 123n, channel_id: 456n } },
    before_id: lastMessageId, // Snowflake ID
    limit: 50
  }
});

// Fetch newer messages (after a certain ID)
const newMessages = await client.sendRequest({
  messages_get_history: {
    chat_ref: { channel: { community_id: 123n, channel_id: 456n } },
    after_id: newestMessageId,
    limit: 50
  }
});

Finding New Content

// Store the last seen message ID
let lastSeenMessageId = getLastSeenMessageId();

// Fetch all new messages since then
const newMessages = messages.filter(m => 
  m.message_id > lastSeenMessageId
);

// Update last seen
if (newMessages.length > 0) {
  lastSeenMessageId = newMessages[newMessages.length - 1].message_id;
  saveLastSeenMessageId(lastSeenMessageId);
}

Deduplication

// Use Snowflake IDs as Map keys for efficient deduplication
const messageMap = new Map<bigint, Message>();

for (const message of incomingMessages) {
  messageMap.set(message.message_id, message);
}

// Convert back to array, guaranteed unique
const uniqueMessages = Array.from(messageMap.values());

Examples from the Protocol

User References

message User {
  // @snowflake<User>
  fixed64 id = 1;
  string name = 2;
  // @snowflake<Emoji>
  optional fixed64 icon = 7;
}
Users are identified by Snowflake IDs, and can optionally have a custom emoji icon (also a Snowflake ID).

Message References

message Message {
  refs.ChatRef chat_ref = 1;
  // @snowflake<Message>
  fixed64 message_id = 2;
  // @snowflake<User>
  fixed64 author_id = 3;
  string message = 4;
  // @snowflake<Message>
  optional fixed64 reply_to = 5;
}
Messages reference:
  • Their own ID
  • The author’s user ID
  • Optionally, another message ID they’re replying to

Community Member Relationships

message CommunityMember {
  // @snowflake<User>
  fixed64 id = 1;
  // @snowflake<Community>
  fixed64 community_id = 2;
  // @snowflake<Role>
  repeated fixed64 role_ids = 3;
}
Community members link together multiple Snowflake types:
  • User ID
  • Community ID
  • Multiple role IDs

Channel Structure

message ChannelRef {
  // @snowflake<Community>
  fixed64 community_id = 1;
  // @snowflake<Channel>
  fixed64 channel_id = 2;
}

message Channel {
  // @snowflake<Channel>
  fixed64 id = 1;
  // @snowflake<Community>
  fixed64 community_id = 2;
  string name = 3;
  // @snowflake<Channel>
  optional fixed64 parent_id = 6;
}
Channels use parent IDs to create hierarchies (categories containing channels).

Best Practices

Treat as Opaque

Don’t parse or manipulate Snowflake IDs unless absolutely necessary. Use them as unique identifiers and for sorting.

Use Correct Types

Always use 64-bit integer types (BigInt in JS, u64 in Rust, uint64 in Go, int in Python). Never use floating point.

Leverage Sortability

Take advantage of the fact that newer items have higher IDs. This is perfect for pagination and finding new content.

Store as Strings (Optional)

When storing in JSON or databases without native 64-bit support, consider using string representation to avoid precision loss.

Common Pitfalls

JSON Serialization in JavaScriptJSON doesn’t support BigInt, so you need custom serialization:
// ❌ WRONG: Will throw error
JSON.stringify({ userId: 1234567890123456789n });

// ✅ CORRECT: Use custom serializer
JSON.stringify(
  { userId: 1234567890123456789n },
  (key, value) => typeof value === 'bigint' ? value.toString() : value
);

// Or use a library like 'json-bigint'
import JSONbig from 'json-bigint';
JSONbig.stringify({ userId: 1234567890123456789n });
Comparison with Regular NumbersMixing BigInt and number in comparisons will throw errors:
// ❌ WRONG: TypeError
if (messageId > 100) { /* ... */ }

// ✅ CORRECT: Both sides BigInt
if (messageId > 100n) { /* ... */ }

Client-Server Communication

See how Snowflake IDs are used in RPC messages

Real-time Updates

Learn how updates reference entities by Snowflake IDs

Message Types

Complete reference for message structures using Snowflake IDs

Build docs developers (and LLMs) love