fix: persistent random Snowflake worker ID + clean reaction API

Two fixes addressing architectural review feedback:

1. Snowflake ID collisions: Replace process.pid-based worker ID with a
   cryptographically random value (0-1023) generated once at first boot
   and persisted to instance_settings.worker_id. Eliminates deterministic
   ID collisions between Docker instances that all run as PID 1.

2. Reaction API leak: Revert addReaction/removeReaction signatures to
   (messageId, emoji) — the store now resolves the channel internally by
   scanning its message cache, keeping routing logic out of the UI layer.
This commit is contained in:
Jannis Braun
2026-03-03 00:03:29 +01:00
parent 5194dbef25
commit 72e07c1cc1
6 changed files with 76 additions and 10 deletions
+12
View File
@@ -3,6 +3,7 @@ import { drizzle } from 'drizzle-orm/better-sqlite3';
import { config } from '../config.js';
import * as schema from './schema.js';
import { runMigrations } from './migrate.js';
import { setWorkerId } from '../utils/snowflake.js';
import { mkdirSync } from 'fs';
import { dirname } from 'path';
@@ -180,6 +181,7 @@ function createTables(db: Database.Database): void {
CREATE TABLE IF NOT EXISTS instance_settings (
id INTEGER PRIMARY KEY DEFAULT 1 CHECK (id = 1),
instance_name TEXT DEFAULT 'Backspace',
worker_id INTEGER,
max_bitrate_kbps INTEGER NOT NULL DEFAULT 20000,
min_bitrate_kbps INTEGER NOT NULL DEFAULT 500,
bitrate_step_kbps INTEGER NOT NULL DEFAULT 500,
@@ -199,6 +201,16 @@ export function initDatabase() {
sqlite.pragma('foreign_keys = ON');
createTables(sqlite);
runMigrations(sqlite);
// Initialize Snowflake worker ID from persisted value (set by migration)
const settings = sqlite.prepare('SELECT worker_id FROM instance_settings WHERE id = 1').get() as { worker_id: number } | undefined;
if (settings?.worker_id !== undefined && settings.worker_id !== null) {
setWorkerId(settings.worker_id);
console.log(`Snowflake worker ID: ${settings.worker_id}`);
} else {
throw new Error('Snowflake worker_id not found in instance_settings — migration failed');
}
console.log(`Database initialized at ${config.dbPath}`);
return drizzle(sqlite, { schema });
}
+21 -1
View File
@@ -1,4 +1,5 @@
import Database from 'better-sqlite3';
import crypto from 'crypto';
import { DEFAULT_EVERYONE_PERMISSIONS, PermissionBits, ALL_PERMISSIONS, permissionsToString } from '@backspace/shared/src/permissions.js';
export function runMigrations(db: Database.Database): void {
@@ -65,7 +66,8 @@ export function runMigrations(db: Database.Database): void {
{
name: 'instance_settings',
columns: [
{ name: 'instance_name', type: "TEXT DEFAULT 'Backspace'" }
{ name: 'instance_name', type: "TEXT DEFAULT 'Backspace'" },
{ name: 'worker_id', type: 'INTEGER' }
]
}
];
@@ -104,6 +106,9 @@ export function runMigrations(db: Database.Database): void {
// ─── Instance settings: ensure default row exists ──────────────────────────
migrateInstanceSettings(db);
// ─── Worker ID: ensure a unique Snowflake worker ID is persisted ───────────
migrateWorkerId(db);
// ─── Admin flag: ensure at least one admin exists (first registered user) ──
migrateFirstAdmin(db);
@@ -133,6 +138,21 @@ function migrateFirstAdmin(db: Database.Database): void {
}
}
/**
* Ensure a unique Snowflake worker ID is persisted for this instance.
* Generated randomly on first boot (0-1023) and never changed.
* This prevents ID collisions between federated instances that would
* otherwise share worker_id = 1 when running as Docker PID 1.
*/
function migrateWorkerId(db: Database.Database): void {
const row = db.prepare('SELECT worker_id FROM instance_settings WHERE id = 1').get() as { worker_id: number | null } | undefined;
if (!row || row.worker_id === null) {
const workerId = crypto.randomInt(0, 1024); // 0-1023 (10-bit range)
db.prepare('UPDATE instance_settings SET worker_id = ? WHERE id = 1').run(workerId);
console.log(`Migrating: Generated Snowflake worker ID ${workerId} for this instance`);
}
}
/** For each server, ensure an @everyone role exists with id === server.id */
function migrateEveryoneRoles(db: Database.Database): void {
const servers = db.prepare('SELECT id FROM servers').all() as { id: string }[];
+1
View File
@@ -180,6 +180,7 @@ export const serverFolderMembers = sqliteTable('server_folder_members', {
export const instanceSettings = sqliteTable('instance_settings', {
id: integer('id').primaryKey().default(1),
instanceName: text('instance_name').default('Backspace'),
workerId: integer('worker_id'),
maxBitrateKbps: integer('max_bitrate_kbps').notNull().default(20000),
minBitrateKbps: integer('min_bitrate_kbps').notNull().default(500),
bitrateStepKbps: integer('bitrate_step_kbps').notNull().default(500),