External Integrations
How the code talks to Minecraft/RCON, GitHub, Resend, OnShape, and Tailscale - from a developer's perspective.
This page explains how the integration code works. For installing and configuring these services on the production server, see the Server Integrations Setup guide instead - that page is ops-focused, this one is code-focused.
Every integration in src/lib/integrations/ follows the same philosophy: degrade gracefully when unconfigured. None of these are required for the app to run - if an env var is missing or the external service is unreachable, the relevant feature shows a "not configured" or "offline" state rather than crashing or throwing on every page load.
Minecraft server status (src/lib/integrations/minecraft.ts)
Uses minecraft-server-util's status() function to perform a Server List Ping - the same protocol the Minecraft client uses to show a server's MOTD/player count in its multiplayer list - against MINECRAFT_SERVER_HOST/MINECRAFT_SERVER_PORT:
const [res, botNames] = await Promise.all([queryStatus(h, p, { timeout: 5_000 }), getBotNames()]);Results are cached in-memory for 30 seconds (reset on server restart) to avoid re-querying the Minecraft server on every dashboard poll. Each player in the sample is flagged isBot by checking membership in the set returned by getBotNames().
Bot detection via RCON (src/lib/integrations/azalea.ts)
The dashboard needs to distinguish real players from Carpet mod bots in the player list and playtime leaderboard. The current approach (see recent commits: "changed bot detection from anonymous player filtering to rcon bot team query") runs an RCON command against a Minecraft scoreboard team named bots:
export async function getBotNames(): Promise<Set<string>> {
if (botNamesCache && botNamesCache.expiresAt > Date.now()) return botNamesCache.names;
const response = await rconQuery("team list bots");
const names = parseTeamList(response);
botNamesCache = { names, expiresAt: Date.now() + 30_000 };
return names;
}rconQuery() opens an RCON connection (new RCON() from minecraft-server-util), races connect+login and command execution each against a 4-second timeout, and returns null on any failure rather than throwing - callers treat "no bot data" the same as "no bots" rather than surfacing an error to the whole status tile. parseTeamList() parses Minecraft's "Team [bots] has N member(s): Name1, Name2" response by splitting on the last colon, which works regardless of the singular/plural wording Minecraft uses. An older heuristic, isOfflineUUID() (checking that a UUID's version nibble is 3, which Mojang only issues to offline/fake players), still exists in the same file but is no longer the primary detection method.
Sending server commands
sendCommand() in the same file is the generic RCON executor used by the server-control card on the /minecraft page to run arbitrary console commands - it requires MINECRAFT_RCON_PASSWORD to be set and connects/logs in/executes/closes per call, same as rconQuery() but without the timeout race (used from an authenticated session, not a background poll).
GitHub org management (src/lib/integrations/github.ts)
Backs the GitHub tab on the /members page - lets members view org members, pending invitations, and teams, and send new invitations, without leaving the dashboard. Talks directly to api.github.com with Authorization: Bearer <GITHUB_TOKEN> and the 2022-11-28 API version header.
The file's own doc comment states the security intent clearly: the token should be a fine-grained PAT scoped only to "Organization → Members: Read and write" - enough to list/invite/remove members, deliberately not enough to promote anyone to org owner. Invitations are always sent as direct_member, and there is intentionally no role-promotion call in this client. If GITHUB_TOKEN/GITHUB_ORG are unset, or the API call fails, the page renders a "not configured"/"unreachable" empty state rather than erroring.
Resend (transactional email)
src/lib/integrations/email.ts is a thin wrapper:
export async function sendEmail({ to, subject, html }) {
const resend = new Resend(process.env.RESEND_API_KEY);
const { error } = await resend.emails.send({
from: process.env.EMAIL_FROM!,
to,
subject,
html,
});
if (error) throw new Error(`Resend error: ${error.message}`);
}Called from two places: src/lib/auth/auth.ts (email verification codes, password-reset codes, and email-change confirmations - see Authentication & Authorization), and src/lib/finance/finance.ts (order-approved / order-denied notifications to the member who submitted the order).
OnShape
src/lib/integrations/onshape.ts follows the same pattern as the GitHub client - a thin authenticated wrapper around OnShape's API (ONSHAPE_BASE_URL/ONSHAPE_ACCESS_KEY/ONSHAPE_SECRET_KEY/ONSHAPE_COMPANY_ID) backing the OnShape tab on the /members page, degrading to a "not configured" state when unset.
OnShape simulation export proxy
src/lib/sim/ is a separate subsystem that turns an OnShape assembly into a robot description file for our Gazebo simulation tooling, fronted by POST /api/sim/export and GET /api/sim/export/{jobId}:
- Auth (
src/lib/sim/request-auth.ts) - the caller sends the club's current 6-digit access code inX-Access-Code. There are no per-user keys and nothing is stored on the caller's machine:src/lib/sim/access-code.tskeeps one code in thesimAccessCodetable, rotates it automatically at 3am Pacific on the next read, and lets any dashboard user rotate it early from the API Keys page. Wrong codes burn a tight per-IP budget (10/hour); correct ones never do. - Cache check (
src/lib/sim/cache.ts) - thesimExportCachetable is keyed on(documentId, workspaceId, elementId)parsed out of the submitted OnShape URL. A hit younger thanSIM_CACHE_TTL_DAYS(default 7) is streamed back immediately unless the caller passedforceRefresh: true. - Export (
src/lib/sim/export.ts) - on a miss,runOnshapeExport()shells out to the externalonshape-to-robotCLI in a temp directory to convert the assembly to URDF, thentars the result intoSIM_CACHE_DIR(default/var/cache/trickfire-sim- see the Server Integrations Setup guide for the directory permissions this requires in production). - Jobs (
src/lib/sim/jobs.ts) - because the export can take a few minutes, it doesn't block the request: the route creates an in-memory job, returns202 { jobId }immediately, and the caller pollsGET /api/sim/export/{jobId}until it returnsdone(streams the archive) orerror. Job state is a plainMap, so it doesn't survive a server restart - fine for a poll-until-done flow, since a restarted export just becomes a new job. - Eviction (
src/lib/sim/cache.ts'sevictIfNeeded()) - after every new export, the oldest cache rows pastSIM_CACHE_MAX_ENTRIES(default 50) are deleted along with their archive files, keyed onlastAccessedAtso frequently-reused exports survive longer.
Leads read the current code off the dashboard's API Keys page (SimAccessCodePanel, backed by src/lib/sim/access-code-actions.ts), which also has a fullscreen view for sharing it at a meeting and a button to rotate it immediately.
Tailscale (Network tab)
src/lib/integrations/network.ts talks to the Tailscale API (TAILSCALE_API_KEY/TAILSCALE_TAILNET) to list devices on the club's shared tailnet and surface join requests, backing the /network page. See the Server Integrations Setup guide for how the API key itself is generated and rotated.
Pl3xMap (world map proxy)
Not a src/lib/integrations client - instead, src/app/pl3xmap/[[...path]] is a catch-all page route that proxies requests through to the Pl3xMap web server (PL3XMAP_URL), so the Minecraft world map can be embedded on the dashboard's own origin without exposing Pl3xMap's port to the internet directly. This is also why next.config.ts carves out a relaxed CSP/header exception specifically for /pl3xmap/* - the embedded map needs permissions the rest of the app's strict headers would otherwise block.