flake.nix only rewrote @/ aliases but left extensionless relative imports
(./router) in compiled dist/. node dist/index.js (how prod runs) cannot
resolve extensionless ESM specifiers -> ERR_MODULE_NOT_FOUND -> backend
crashlooped (444 restarts, port 4001 dead). Extract the fixer into a shared
scripts/fix-imports.mjs that appends .js to extensionless relative imports and
rewrites @/ aliases, and wire it into backend + discord-gateway build phases.
Verified: fresh tsc + fixer -> node dist/index.js boots; oRPC over /trpc
serves both HTTP POST and WebSocket (config/dashboard/voice/moderation/
media/chatbot/analysis) end-to-end against Postgres + Redis. next build
passes with the oRPC client + partysocket.
dontPatchELF only disabled the patchELF sub-phase; fixupPhase's
shrinkELF step still emits the same error on the prebuilt .node addons
and .o/.a object files in node_modules. Skip the entire fixupPhase
(dontFixup = true) for the gateway — node is the external interpreter
and .node addons are self-contained dlopen prebuilts, so Nix RPATH
patching/stripping is neither needed nor wanted.
Add dontPatchELF = true to the discord-gateway derivation. Nix's
fixupPhase runs patchELF over $out/node_modules and chokes on the
non-ET_DYN ELF files (.o/.a objects + prebuilt .node addons), emitting
hundreds of non-fatal 'patchelf: wrong ELF type' lines per build. The
real binary is node (external, RPATH-fixed) and the .node addons are
self-contained prebuilts loaded via dlopen, so Nix RPATH patching is
neither needed nor wanted. Shebang patching still runs.
Drop npm_config_build_from_source=true so node-pre-gyp downloads the
published prebuilt .node for Node 22 (ABI node-v127, linux-x64-glibc-2.35)
instead of compiling libopus C++ every build. Replace the hardcoded
'npm run install' (node-gyp compile) loop with 'pnpm rebuild @discordjs/opus'
which runs the package's own install script (prebuilt fetch, source build
only as fallback). sharp already uses @img prebuilt packages (its install
script failure is non-fatal), so only opus was actually compiling.
binding.gyp appended '/libdatachannel.so.0.24.0' to LDC_LIB; nixpkgs output
layout is <out>/lib/libdatachannel.so.0.24.1. Make LDC_LIB the complete
library path (env or default) and drop the append.
libdatachannel-src fetchFromGitHub + manual cmake build fails: GitHub tarball
does not include git submodules (deps/plog, libjuice, libsrtp, usrsctp) →
CMake 'source directory does not contain CMakeLists.txt'.
Switch to pkgs.libdatachannel (0.24.1): nixpkgs builds submodules + ships
lib/dev outputs. In the Nix sandbox everything is consistent (store glibc),
so the GLIBC_ABI_GNU2_TLS issue that blocks host-local use of 0.24.1 does
not apply to the Nix build. binding.gyp defaults stay on local 0.24.0 for
dev; Nix sets LDC_INCLUDE/LDC_LIB to the store paths.
- Replace the per-package rebuild loop (node-datachannel cmake-js, zeromq)
with: opus build + libdatachannel-min N-API binding build (fetchFromGitHub
libdatachannel v0.24.0 — pinned because nixpkgs 0.24.1 is glibc-incompatible
with this host; sha256 1jk53qs…).
- Removes ~760MB of node-datachannel build/cleanup cruft from the build
phase; node_modules now 423MB (was 1.5GB).
gateway output 1.4G -> 424M (-70%), backend 198M -> 60M (-70%).
- pruneProd: delete every .pnpm dir not in 'pnpm list --prod' graph
(biome/typescript/esbuild/drizzle-kit/vitest/tsx ~150MB+) then drop
dangling symlinks (top-level, scoped dirs, hoist, .bin) so stdenv
noBrokenSymlinks fixup passes.
- NOT using 'pnpm install --prod': it collapses the public-hoist dir
(.pnpm/node_modules) that peer resolution relies on for
@lng2004/node-datachannel + @seydx/node-av-linux-x64 (voice breaks).
- node-datachannel: strip build/_deps (cmake FetchContent ~380MB) +
nested node_modules (nw-gyp/typescript/puppeteer ~380MB) after
compile; runtime needs only build/Release/node_datachannel.node.
- verified: native binaries (datachannel/opus/zeromq) intact, all 27
runtime modules resolve, dev tools 0.
noBrokenSymlinks fails frontend build: .next/standalone/node_modules/.pnpm/
node_modules/semver -> missing target. The standalone server never resolves
pnpm's hoisted .pnpm dir (it bundles its own node_modules) — delete broken
symlinks before install so stdenv check passes.
Rombak total alur data frontend: dari static-export CSR (tiap browser
fetch sendiri + akumulasi state voice per-tab) jadi server-side rendering.
Frontend (Next.js):
- next.config: output export -> standalone; halaman jadi server components
- server data layer baru src/lib/api/server.ts (GMW_BACKEND_URL, no window)
- dashboard/media/messages/moderation/recordings/voice page -> RSC yang
fetch backend di render-time, seed ke client view (SWR fallbackData)
- hook-hook utama terima initialData -> first paint data server, revalidate
SWR setelahnya, tanpa spinner-blank-load
- messages: guild/channel/tab/selected dibaca dari URL di server, page awal
di-fetch server-side
Shared realtime state (voice) server-authoritative:
- backend src/modules/voice/live-speaker.ts: agregat voice_active_user dari
gateway jadi snapshot authoritatif (single source of truth semua browser)
- GET /api/voice/status kini include activeSpeakers
- WS initial states kirim voice_state snapshot saat connect (late join
langsung dapat state yang sama, bukan daftar kosong)
- useSpeakers seed dari server snapshot + voice_state full-replace +
voice_active_user delta upsert
Deploy:
- flake.nix: frontend package build SSR standalone (server.js wrapper,
GMW_FRONTEND_PORT=4017); proxy nginx template proxy / -> Next server,
/api + /ws tetap ke backend :4001
path: literals in flakes do NOT respect .gitignore, so a dirty local
out/ (stale chunks from previous builds, e.g. 3y39nidcm2n_s.js from
the removed quick-prompt button) leaked into the sandbox and got
served forever. Add filterSource helper that excludes out, .next,
node_modules, pnpm-lock.yaml from the frontend derivation source.
Root cause voice tidak berfungsi di produksi: ffmpeg & yt-dlp cuma ada
di devShell, bukan di package discord-gateway. Bukti dari log gateway:
'FFmpeg/avconv not found!' saat voice:transmit:start (mic -> Discord),
yang juga mematikan music playback (StreamType.Arbitrary butuh ffmpeg)
dan segment muxing rekaman.
- buildInputs: pkgs.ffmpeg-headless + pkgs.yt-dlp
- wrapper export PATH ke keduanya sebelum exec node
Verifikasi: nix build PASS; closure berisi ffmpeg-8.1.2 + yt-dlp-2026.07.04;
wrapper PATH mengarah ke keduanya; ffmpeg/yt-dlp jalan.
process.cwd() defaults to / (systemd default) without an
explicit WorkingDirectory. The migration code looks for
drizzle/migrations/meta/_journal.json relative to cwd,
so the wrapper must cd into the package directory first.