Shippers. // the engine room

technical field notes · v1

the engine room.

the operator doesn't fix the engines by hand any more. the operator still knows what a healthy one sounds like.

the manual says what the operator does. this page says what the machinery is: the languages, runtimes, package managers, frameworks, build tools and pipelines the fleet actually runs on. every concept is taught off a real incident from a shippers codebase, because the fleet's own wreckage is the best syllabus there is.

typescriptnode · bunpnpm next.js · firebaseexpo · supabasevite · parcel vercel · netlify · github actions
00

00 · why this exists

dispatch heavily, read fluently.

v2 doctrine moved the hands from the keyboard to the gates. that only works if the operator can read what comes back through them. you don't need to write every line. you do need to read every stack trace, every diff at a gate, every standup an agent files. an operator who can't read the telemetry isn't operating, they're hoping.

three reading skills carry almost all of it:

the error
the last line of a stack trace is the symptom. the first line you recognise is usually the cause. warnings above the error are frequently the actual answer, as the pnpm case below proves.
the diff
at a pre-deploy gate you're not reviewing style, you're asking two questions. what state does this touch, and what happens when it runs twice.
the config
most "worked yesterday" failures are version-gated behaviour changes in a tool's defaults, not bugs in your code. reading changelogs is a production skill.
01

01 · the languages

one language, three dialects.

the fleet is effectively a typescript shop end to end. everything else is a supporting dialect you read more than you write.

typescript

javascript with a type system compiled away before the code runs. the browser and node never see typescript; a compiler (tsc, or faster tools like esbuild and swc) strips the types and emits plain javascript. the types exist purely at build time, which is why they're called a static type system: they catch the mismatch before the code ships, not after a patient record fails to save at 2am.

the payoff
a function that expects clinicId: string refuses to compile when handed undefined. in a multi-tenant system like strydeos, where every write must be scoped to the authed clinic, that refusal is a security control, not a nicety.
tsconfig.json
the compiler's contract. strict: true is non-negotiable on new work. the paths field is where @/components aliases live, and it needs matching config in the bundler or imports resolve in the editor and die in the build.
where it runs
strydeos (next.js), trademind (expo + supabase edge functions), driiva (react/vite + expo), every agent and mcp server in the software bucket.

sql

trademind sits on supabase, which is managed postgres. every query the app makes is sql underneath, even when a client library hides it. the two ideas worth owning: row level security, where the database itself enforces who can read which rows, and rpc functions, stored procedures the client can call directly. both featured in a real trademind finding, covered in section 03.

bash + the config dialects

bash is the glue of every pipeline: deploy scripts, ci steps, the init and bundle scripts in skills. read it well enough to spot set -e (exit on first error) and to follow a pipe. the config dialects are json (package.json, tsconfig), yaml (github actions workflows, pnpm-workspace.yaml) and toml (some rust-era tools). yaml's significant whitespace breaks more ci runs than logic errors do.

02

02 · runtimes + package managers

where the code runs, and who fetches its parts.

a runtime executes your javascript outside the browser. a package manager assembles the thousands of third-party parts your code imports. the second one just cost a working session, so it gets the case file.

runtimes

node
the default. v8 engine lifted out of chrome. everything on the fleet runs on it today. current lts lines are even-numbered (20, 22).
bun
the new-project bias. runtime, package manager, bundler and test runner in one binary, dramatically faster installs. use it where nothing forces node.
edge runtimes
cloudflare workers and vercel edge run a stripped v8 with no filesystem and tight limits. supabase edge functions run deno. code written for node does not automatically run there.

what a package manager actually does

resolve
reads package.json, walks the full dependency graph. your 30 direct dependencies become 400+ transitive ones.
lock
writes the exact resolved version of every package to a lockfile (pnpm-lock.yaml, package-lock.json). the lockfile is why a build is reproducible. commit it, always.
install
downloads into node_modules. pnpm hard-links from a global store instead of copying, which is why it's fast and disk-cheap.
semver
^4.2.1 means any 4.x.x. majors are allowed to break you. a major bump in a tool's defaults is exactly what the case below is.
lifecycle scripts
packages can declare postinstall scripts that run arbitrary code on your machine at install time. native modules (c/c++ compiled against node, like parcel's file watcher or swc's compiler core) genuinely need them. malware also loves them. hold that thought.
case file 01 · pnpm 11 · the silent hard gate

the bundle that refused to build

context: bundling a react + vite app into a single-file html artifact. the bundle script (written against pnpm 10) shells out to parcel. parcel runs a dependency check. the check runs pnpm install. and everything dies with a stack trace that points at pnpm internals, not at the actual problem.

$ pnpm exec parcel build index.html --dist-dir dist --no-source-maps [ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: @parcel/watcher@2.5.6, @swc/core@1.15.43, lmdb@2.8.5, msgpackr-extract@3.0.4 Run "pnpm approve-builds" to pick which dependencies should run scripts. [ERROR] Command failed with exit code 1: pnpm install

root cause, two layers deep. after the 2024/25 npm supply-chain attacks, pnpm 10 stopped running lifecycle scripts by default: a security feature, deliberately inconvenient. under pnpm 10 that produced a warning and a broken-but-quiet install. pnpm 11 hardened it into a fatal exit 1. so a script that worked last quarter dies this quarter with zero code changes. version-gated default, the classic.

the trap inside the trap. the documented fix used to be an onlyBuiltDependencies allowlist under the pnpm field in package.json. pnpm 11 retired that field entirely and moved config to pnpm-workspace.yaml. applying the old fix produces this, and it's a warning, so it scrolls straight past:

[WARN] The "pnpm" field in package.json is no longer read by pnpm. The following keys were ignored: "pnpm.onlyBuiltDependencies".

the fix, then the proof. approve the native builds deliberately (know what you're approving: watcher, compiler core, database bindings, all legitimately need to compile), then verify with an exit code, not vibes.

$ pnpm approve-builds --all .../@parcel/watcher install: Done .../@swc/core postinstall: Done $ pnpm install; echo $? 0 $ pnpm exec parcel build index.html --dist-dir dist --no-source-maps √ Built in 7.63s
l.1

the warning above the error was the answer. errors tell you something broke; warnings tell you why. read up the trace, not just the last line.

l.2

security defaults are features. the right move is a deliberate allowlist, not disabling the guard. a postinstall script runs with your full user permissions.

l.3

"worked yesterday" almost always means a tool's defaults moved under you. pin versions in scripts you expect to survive quarters, or write the version guard in.

l.4

lesson becomes skill. this trace is now a patch on the bundle script, not a memory. nothing gets learned twice.

03

03 · the fleet's stacks

three engines, three architectures.

a framework is the opinionated shell around your code: routing, rendering, data flow. the fleet runs three deliberately different ones, and each has already produced at least one incident worth teaching from.

strydeos

locked
stack
next.js on vercel, firebase for auth, firestore and cloud functions. locked means locked: no migration proposals, the leverage is in shipping on it.
next.js
react with a server. pages can render on the server (fast first paint, seo) or the client (interactivity), and api routes live in the same repo. vercel is the deployment platform built by the same company, which is why the pairing is frictionless.
firestore
a nosql document database. no tables, no joins: collections of json-like documents, queried by index. reads are shallow and fast; the trade-off is that query power is bounded by what you indexed in advance.
case file 02 · firestore · the cron that died silently

the gdpr hard-delete job stopped running. root cause: the query filtered the patients collection on more than one field, and firestore requires an explicit composite index for that. no index, and the query doesn't degrade, it throws. the cron caught nothing, logged nowhere useful, and died on schedule for weeks. a compliance job, dead, silently.

// the shape of the offending pattern db.collection('patients') .where('clinicId', '==', clinicId) .where('deletionDue', '<=', now) // equality + range = composite index required
l.5

nosql indexes are explicit contracts, declared in firestore.indexes.json and deployed like code. a query that works in the emulator can still throw in prod.

l.6

scheduled jobs obey the fleet rule: nothing runs without a heartbeat. a cron that can fail silently is a cron that already has.

trademind

builds · active contract
stack
expo sdk 54 (react native for ios/android from one typescript codebase), supabase eu west (postgres + auth + storage + edge functions), claude api, elevenlabs convai + twilio for voice, stripe connect for payouts.
expo
react native with the toolchain solved: build service, over-the-air updates, testflight distribution. the code is react, but it compiles against native ios/android modules, which is where the next case file comes from.
supabase
managed postgres wearing a firebase costume. relational data, real sql, and row level security policies enforced by the database itself.
case file 03 · native modules · the launch crash

the app crashed on launch in a testflight build. the pattern: a native module loaded at import time, at the top of a file, before the app had initialised. in javascript, imports execute the moment the file is parsed. the fix was a lazy require: defer loading the module until the code path that needs it actually runs.

// crashes at parse time if the native side isn't ready import Heavy from 'heavy-native-module' // loads only when called, after init const doWork = () => { const Heavy = require('heavy-native-module'); ... }
l.7

mobile shipped means verified on-device. this crash never appears in tests or the simulator config that hid it. tests green is not shipped.

case file 04 · supabase rls · the anon-executable mint

a codebase review flagged rpc functions callable by the anon role, the unauthenticated public key shipped inside the app, that could mint records. postgres functions marked security definer run with the function owner's privileges and bypass row level security unless you re-check auth inside them. p1 finding: anyone with the public url could have called them.

l.8

the anon key is public by design. every rpc it can reach is attack surface. default to revoke execute from anon and grant back deliberately, per function.

driiva

deprioritised · post-raise
stack
react + vite on the web, expo sdk 52 on mobile, firebase underneath. two clients, one backend.
the incident class
the memorable one was auth taking 27 seconds on load, since fixed. it belongs to the family production-bug-hunter exists for: auth races, stale listeners, initialisation order. firebase auth is asynchronous and event-driven; render before the auth state event lands and you get flicker, hangs, or double work.
l.9

anything event-driven needs a timeout and a fallback path. "it usually arrives quickly" is not an architecture.

shippers-tt · this site

static · netlify
stack
static html deployed to netlify by manual drag-and-drop, no linked git repo. deliberately boring hosting.
the one live part
/api/tasks is a netlify serverless function: a single javascript function netlify runs on demand, holding the notion token server-side and pulling the timetable tasks database live. notion edits go live without a redeploy because the data was never baked into the html. that split, static shell + live function, is the cheapest useful architecture on the internet.
04

04 · build tools

from source to artefact.

nothing you write ships as written. a toolchain transpiles it, resolves it, bundles it, minifies it. knowing the stages is what lets you read a build failure without guessing.

the stages

transpile
typescript and modern syntax down to javascript engines understand. tsc is the reference; esbuild and swc do it 10-100x faster and power everything current.
bundle
walk every import from an entry file, produce a handful of output files. resolves the module graph, deduplicates, splits code by route.
tree-shake
drop exports nothing imports. why importing one function from a big library doesn't ship the whole library.
minify
shrink names, strip whitespace and dead branches. the reason prod stack traces are unreadable without source maps.

the tools on the fleet

vite
the dev server + build tool for driiva web and most new react work. dev mode serves modules natively for instant hot reload; prod builds bundle through rollup.
next's compiler
strydeos builds through next.js, swc underneath, orchestrated by vercel's ci.
metro
expo's bundler for trademind and driiva mobile. same job, different constraints: it bundles for a phone, not a browser.
parcel
zero-config bundler, used in the artifact pipeline for its party trick: inlining an entire react app, css, fonts and all, into one self-contained 358kb html file. one file, no server, opens anywhere.
05

05 · shipping + devops

the pipeline is the product.

devops is everything between "it works here" and a real user completing the flow. the fleet's definitions apply: deployed means nothing, shipped means end-to-end without intervention, paid is the only done for builds.

git, with branch safety

the standing rule: no ai-assisted change lands on strydeos or driiva main directly. work happens on a branch (fix/ava-dead-line is a live example), ships through a pull request, and the diff gets read at the gate. branches are the undo button for a fleet: a bad agent run on a branch is a deleted branch, a bad agent run on main is an incident.

ci/cd + preview deploys

continuous integration runs your checks on every push: github actions workflows, defined in yaml, running tests, lint and builds on a clean machine. continuous deployment takes green builds live. vercel and netlify add preview deploys: every pull request gets its own throwaway url, so a gate review looks at a running thing, not a diff and a promise.

the classic failure class is environment mismatch: works locally, dies in ci. real fleet example: a vercel build that gated on the commit author's github org association. the code was fine; the pipeline's rules were the bug. ci config is code, read it like code.

secrets + telemetry

env vars keep secrets out of the repo: platform-side (vercel, netlify, github secrets), never committed, split per environment. two hard rules from a live strydeos audit. one, never log request payloads that can carry keys: an api key surfaced in error logs, and the only correct response to an exposed key is immediate rotation, revoke and reissue, not deletion of the log. two, error monitoring is an instrument: sentry catching exceptions in prod is the fleet dial for code health, and eu region residency matters when the data is clinical.

l.10

a secret that touched a log is burned. rotate first, investigate second.

06

06 · the claude code side

the operator codes through a harness.

this is the side that actually changed. the keyboard work moved to agents in claude code; what the operator writes is the harness around them. a harness is a prompt with a structure that survives contact: role, context, bugs in order, fix, verify. no verify command, no claim.

the pnpm case, as it would be dispatched

the same incident from case file 01, written as the harness you'd paste into claude code. notice the shape: the agent gets the trace, the constraint, and the definition of done as an exit code. it does not get room to guess.

# role you are debugging a pnpm build failure in a react + vite project. do not disable security features; find the sanctioned fix. # context pnpm 11.10.0, node 22. `pnpm exec parcel build` fails. trace attached. the script previously worked under pnpm 10. # bugs, in order 1. ERR_PNPM_IGNORED_BUILDS on @parcel/watcher, @swc/core, lmdb, msgpackr-extract 2. a WARN says the package.json "pnpm" field is no longer read # fix work on a branch. smallest change that makes the install and build pass. prefer the tool's own approval mechanism over config workarounds. # verify `pnpm install; echo $?` prints 0, and `pnpm exec parcel build index.html --dist-dir dist --no-source-maps` completes. paste both outputs.
gates
the operator steps in at three rings: scope (the harness above), pre-deploy (the diff and the preview url), sign-off. an agent that guesses past a gate gets pulled.
fan-out
independent legs never queue. the pattern for multi-screen builds: lock the shared contracts first (types, api layer, tokens, navigation), then one subagent per screen with tests as the merge gate.
tdd + characterisation
new behaviour gets a failing test first. big rewrites gate behind a characterisation suite that passes against the current code before a line changes: pin what is, then move it.
model tiering
frontier models for decision-heavy legs (ambiguous specs, architecture, security-sensitive, non-reproducible bugs). cheaper tiers for mechanical execution against known patterns. spend follows ambiguity, not volume.
breadcrumbs
long-running work leaves commits, a progress file, a standup. quiet equals not finished. before a context clear, the learning gets lifted into a skill or claude.md, which is exactly how case file 01 became a bundle-script patch.
07

07 · glossary

the terms, at a glance.

lockfileexact resolved version of every dependency. the reproducibility contract. commit it.
semvermajor.minor.patch. majors may break you. ^ pins the major only.
lifecycle scriptcode a package runs on your machine at install time. useful, and the main supply-chain attack vector.
native modulec/c++ compiled against your node version. needs build scripts. breaks on runtime upgrades.
transpileconvert source (typescript, new syntax) to javascript engines can run.
tree-shakedrop code nothing imports at bundle time.
hmrhot module replacement. dev server swaps changed code in place without a full reload.
composite indexfirestore index across multiple fields. required for multi-field queries, or they throw.
rlsrow level security. postgres enforcing per-row access in the database itself.
security definera postgres function that runs with its owner's privileges, bypassing rls unless it re-checks auth.
anon keysupabase's public client key. shipped in the app, safe only because rls does the guarding.
edge functiona small function running on a stripped runtime close to the user. no filesystem, tight limits.
preview deploya throwaway live url per pull request. review the running thing, not the promise.
race conditioncorrectness depends on which async event lands first. the auth-flicker family.
idempotentsafe to run twice. the property every webhook handler and cron must have.
source mapthe file that translates minified prod stack traces back to your source lines.
characterisation suitetests that pin current behaviour before a rewrite. the gate big changes hide behind.
monorepomany packages, one repo, one lockfile. pnpm workspaces exist for this.