Start here — the whole shared inventory, in one place.
Every @broberg/* package by category, and every hard-won tip we've captured. Reuse > re-roll — skim this before you wire anything, and enroll when you adopt.
Packages by category
@broberg/themev0.3.1The design-token foundation every surface inherits
@broberg/stack-b-baseplannedA ready-to-run base scaffold for Stack B apps (Bun · Hono · Preact · Tailwind v4) so a new lightweight service boots with the house wiring already …
@broberg/stack-a-baseplannedThe Stack A counterpart
@broberg/configv0.2.0The fleet's single-source config helper
@broberg/mailv0.3.0The fleet's thin Resend send primitive
@broberg/mediav0.2.1The fleet's provider-agnostic media-storage facade
@broberg/mail-corev0.1.0DELIVERED by @broberg/mail-core (F023.1)
@broberg/media-transformv0.1.0The fleet's server-side image-transform primitive
@broberg/cronv0.1.0The fleet's typed self-service client for cronjobs.webhouse.net
@broberg/webpushv0.1.1The fleet's storage-agnostic Web Push primitive
@broberg/mcpv0.4.0The fleet's genuinely-reusable (NOT slim) MCP-server toolkit
@broberg/secret-scanv0.1.7Pure, dependency-free secret/credential redaction
@broberg/lensv0.1.3A headless POST /api/lens-session mint endpoint (+ thin Next.js / Hono adapters) that issues a short-lived, read-only Playwright session so Cardmem…
@broberg/authv0.1.2Thin fleet wrapper around Better Auth
@broberg/apikeyv0.1.1The fleet's inbound API-key primitives
@broberg/event-logv0.1.0A GDPR-aware event and activity log
@broberg/gravatarv0.1.0Isomorphic Gravatar helper (SHA-256 via crypto.subtle) + initials fallback
@broberg/consent-cookiev0.1.0A consent and cookie banner
@broberg/themev0.3.1DELIVERED by @broberg/theme (0.3.1)
@broberg/ui-controls-corev0.1.0The custom-control kit
@broberg/cmdkv0.1.0A Cmd+K command palette
@broberg/i18nv0.1.0i18n and a language switch
@broberg/seoplannedSEO and metadata helpers for Stack A
@broberg/mail-corev0.1.0Branded HTML email shell + primitives (renderShell, heading/paragraph/cta/factBox/signOff, CID logo attachment)
@broberg/speech-dictionaryv0.1.1Fleet STT vocabulary + correction primitive
@broberg/notifyv0.1.0Dark-ship team-chat webhook notifications
@broberg/lens-enginev0.4.1The shared Playwright capture + flow engine for the cardmem-lens fleet
@broberg/lens-clientv0.1.0Thin, typed client for the HOSTED Lens (lens.cardmem.com)
@broberg/bodymapv0.2.4Interactive body pain-map
@broberg/stripev0.2.0The fleet's one Stripe chokepoint
@broberg/pwav0.2.2The fleet's PWA primitive
@broberg/forms-turnstilev0.1.0A spam-protected public-form pipeline
@broberg/httpv0.1.0Framework-free, zero-dep HTTP header/response primitives for Node/Bun/edge
@broberg/seti-clientv0.3.2The SETI streaming-chat client
@broberg/seti-serverv0.2.5The SETI proxy router
@broberg/soundkitv0.1.0SoundKit
@broberg/deploy-corev0.1.0The fleet's shared deploy execution layer
@broberg/changelogplannedAuto product-changelog from git history
@broberg/db-sdkv0.1.0The fleet Data SDK
@broberg/ai-sdkv0.25.1The fleet LLM SDK
@upmetrics/sdkv0.3.1The fleet telemetry SDK
upmetrics-swiftv0.1.0Native iOS/macOS error + crash reporting
@broberg/fleet-clientv0.1.0The typed fleet-comms client
@broberg/fleet-contractsv0.1.0The fleet-comms contracts
@broberg/complimenta-sdkv0.2.0Typed client SDK for the Complimenta booking API
@broberg/cms-inline-editv0.4.14Click-to-edit widget for live @webhouse/cms-sites
@broberg/cms-chat-clientv0.4.16Shared quick-action cache-client for the CMS chat surface
Tips & tricks — 109 across 14 platforms
Fly.io24
- regionRegion is ALWAYS arn (Stockholm). Set primary_region = "arn" — never US/Amsterdam.— components
- costIdle-cheap services: auto_stop_machines = "stop" + auto_start_machines = true + min_machines_running = 0. Cold-start ~1s.— components
- deployfly deploy --remote-only builds on Fly's builder — no local Docker daemon needed.— components
- secretsSecrets via flyctl secrets set KEY=val (encrypted, injected at runtime). Never bake into the image or commit.— components
- tlsCustom domain: fly certs add <domain> first; the cert validates by itself once the DNS record resolves.— components
- opsDebug live: fly logs -a <app>, fly ssh console -a <app>, fly status. Health check on /health in [[http_service.checks]].— components
- ssh-not-shellflyctl ssh console -C does NOT parse as a shell — argv is split, so &&, ;, |, >, * become LITERAL args to the binary. One -C wiped all of /data once (rm got four path-args). One command per -C; for more, sftp a script then -C 'bash /tmp/x.sh'.— trail
- destructive-opsBefore any rm -rf on a prod volume: snapshot first (flyctl volumes snapshots create <vol>) — auto-snapshots are only 5-day retention. Prefer find <path> -maxdepth 1 -name X -exec rm -rf {} + so a metachar can't widen the blast radius.— trail
- auth-warm-machineStateful auth-apps need min_machines_running = 1 — autostop cold-starts lose in-flight WAL writes and drift OAuth state-cookies across instances (sessions drop mid-flight). ~$2-5/mo kills the bug class.— trail
- sizingA cc-session in a Fly container needs >=2gb RAM — less and the OOM-killer hits it under prompt-load. Use machine-managed launch for long-running edge agents.— buddy
- deploySingle-machine app: deploy with --ha=false, else Fly spins a 2nd machine you didn't ask for. The 'not listening on expected address' smoke warning is transient — 'reached good state' + 'DNS verified' are what count.— upmetrics
- statefulFilesystem-stateful app (one volume) must NEVER fly scale count >1 / run multiple machines on the same volume — each gets its own copy then silent data divergence. Stay single-machine until state moves to a shared DB (Turso).— cms
- sqliteSQLite + Litestream = ONE writer. Single volume + --ha=false; never multiple machines against the same volume (corruption).— upmetrics
- permissionsflyctl ssh writes as root, so files become root-owned and a non-root runtime user (e.g. uid 1001) gets EACCES writing them. Don't write runtime-writable paths via SSH; use the app's HTTP API, or chown -R in the same session (or at boot via gosu in the entrypoint).— cms
- deploy-resilienceCI builder down (depot timeout)? Build arm64 locally then a Dockerfile.prebuilt that COPYs the prebuilt dist (skips vite-under-qemu) + flyctl deploy --local-only. Rescues prod when CD is red.— cardmem
- spa-cacheSPA shell (index.html) MUST be Cache-Control: no-cache, else a stale index serves the old bundle after deploy. Verify on bundle-hash/content-marker, never curl-200.— cardmem
- ssh-secretsRun one-off in-container scripts without leaking secrets: base64-encode a small script and -C 'sh -c ...base64 -d > /tmp/x.js && bun /tmp/x.js; rm /tmp/x.js'. Secrets stay in the container; only the result comes out.— upmetrics
- docker-diskRepeated local Docker builds (e.g. fly deploy --local-only during a CI outage) fill the Docker VM's disk via build-cache → 'No space left on device' mid-build. Fix: docker builder prune -f && docker image prune -f (frees only unused; ~12GB back). Better: a prune step BEFORE each bypass-deploy, or bump Docker Desktop's disk allocation.— cardmem
- deploy-resilienceThe CI-outage deploy bypass (Dockerfile.prebuilt + a fly.toml dockerfile-override + .dockerignore negation) is TEMPORARY — NEVER commit it, it breaks normal CD. Revert to depot / normal CD the moment the builder is back.— cardmem
- warm-for-queriedmin_machines_running=1 ALSO for a service the fleet QUERIES or health-probes (e.g. discovery.broberg.ai), not just stateful auth-apps: with min=0 the first request after autostop scales-to-zero eats the cold-start or times out at the edge → a user reads it as DOWN. ~$2/mo keeps one warm. (broberg-discovery hit exactly this.)— components
- secrets-flipEmergency flag-flip without a rebuild: flyctl secrets set KEY=val applies via a ~30s machine restart (overrides image-baked ENV). Good to unstick prod fast — then fix the durable source (fly.toml [env]) so a later deploy doesn't revert it.— cardmem
- chromium-on-flyHeavy SYNCHRONOUS CPU work in an API route on shared-cpu-1x/1GB (Chromium screenshot→PDF: large PNG frames + pdf-lib.embedPng) can take down the WHOLE machine — it blocks the single core's event loop long enough that Fly's port health-check fails → the proxy 503s ALL traffic ('could not find a good candidate within 40 attempts') and the Chromium child is OOM-reaped. Fix (all three): (1) deviceScaleFactor:1 not 2 — ~4× lighter frames + far faster embed, fidelity fine for PDF (biggest single win); (2) serialize jobs via a module-level promise-chain so only ONE render runs at a time (no parallel Chromium); (3) cache the output on the PERSISTENT volume, mtime-invalidated (regenerate only if a source file is newer) — repeated calls went 52s → 0.3s. Verify the machine STAYS UP by polling a cheap endpoint (/login) every 6s DURING generation — prove it stays 200, not just that the PDF came back. (An app's OWN runtime render legitimately uses playwright-core; the no-raw-Playwright rule — cardmem Lens F112 — governs VERIFICATION automation, not a shipped render feature.)— pitch-vault
- remote-builder-esbuildFly's REMOTE builder can't run esbuild — the esbuild service dies mid-build (EPIPE / 'service was stopped') under BOTH bun and node, so a Vite/esbuild bundle step INSIDE `flyctl deploy --remote-only` fails. Fix: build the Vite/esbuild dist on the host or in CI FIRST, then COPY the prebuilt dist into the image (the Dockerfile does no esbuild) — let the remote builder only assemble the image, not bundle.— happy-little-place
- server-deploy-workflowServer-app Fly deploy (Bun/Hono + custom Docker) → reuse the fleet-shared reusable workflow instead of hand-rolling a per-repo deploy job: `uses: broberg-ai/components/.github/workflows/fly-server-deploy.yml@main` (workflow_call). It does the DEPLOY half — optional host-build (the esbuild-on-host workaround) → flyctl deploy --remote-only → health-verify (polls your health_url for 200, fails if never) → optional required-secrets presence check. Ship-dark: no FLY_API_TOKEN → deploy skipped, job stays green. The CALLER owns the test gate (`deploy: needs: test`). See components F033.7.— components
Cloudflare13
- dnsCNAME → a Fly app MUST be DNS-only (grey cloud), not proxied (orange) — else Fly's TLS cert validation fails.— components
- turnstileTurnstile site-key from a runtime config endpoint (not a build-time env) → rotate keys without rebuild/redeploy.— xrt81
- turnstileTurnstile sites are domain-scoped — each project needs its own site (keys aren't reusable across domains).— xrt81
- storageObject storage = R2; consume via @broberg/media (provider-agnostic facade, R2 provider) rather than rolling raw S3 calls.— components
- dnsPrefer CNAME over A/AAAA when pointing at Fly — survives Fly IP changes, no hardcoded IPs. TTL auto (Cloudflare-managed).— buddy
- gdprR2 endpoint MUST be the .eu. host (https://<acct>.eu.r2.cloudflarestorage.com) for EU residency — without .eu. you get US. Presigned GET (no public bucket); multi-tenant via key-prefix.— cardmem
- tokensAn app-scoped CF_API_TOKEN (Pages/DNS/Turnstile) does NOT carry R2 Storage:Edit or User API Tokens:Edit — R2 needs a separately-scoped token.— cms
- tlsIncomplete TLS cert chain (wrong/missing intermediate) makes Node/Bun strict TLS fail 'unable to verify the first certificate' while curl/browsers tolerate it. Symptom: works in curl, fails in a server-runtime fetch.— fdaa
- cert-orderingCustom-domain cert ordering (GitHub Pages et al.): set DNS FIRST, wait ~30s to propagate, THEN attach the custom domain — the platform runs its DNS check at attach-time and queues the cert immediately; reverse order parks the request 25+ min.— cms
- dns-verifyA local dig/curl returning NXDOMAIN can be a STALE macOS mDNSResponder negative-cache (shared by every local session), not a real missing record. Verify against a public resolver — dig @1.1.1.1 <host> / curl --resolve — before calling a domain dead. (This nearly stalled a 15-repo rollout on a false alarm.)— cardmem
- r2-provisioningNeed an R2 bucket? Provision it 100% programmatically (NO dashboard) via dns-mcp's R2Client / MCP tools: r2_list_buckets · r2_create_bucket · r2_create_scoped_token. Creates an EU-jurisdiction bucket + scoped S3 creds (access_key_id / secret / endpoint). EU jurisdiction is set AT creation and is IMMUTABLE → endpoint https://<acct>.eu.r2.cloudflarestorage.com. Proven live (bucket vnleker + read_write creds, S3-list 200).— buddy
- r2-tokenR2 provisioning needs a token scoped Workers R2 Storage + User API Tokens Write (dns-mcp's CF_BOOTSTRAP_TOKEN, separate from CF_API_TOKEN). The ordinary DNS/zone-scoped CF_API_TOKEN CANNOT do R2 — you get an auth error. Don't waste time debugging the wrong token.— buddy
- r2-creds-secrecyRaw S3 creds from a scoped-token mint (access_key_id / secret / endpoint) go straight into the consumer's gitignored .env — NEVER over intercom or any chat surface. Treat them like any other secret.— buddy
Resend8
- reuseUse @broberg/mail — don't hand-roll a Resend client. createMailer({apiKey, from, allowlist}) keeps your own env-var names.— components
- domainsSend only From a VERIFIED domain (Resend → Domains). An unverified From fails or tanks deliverability.— components
- safetyDev/preview: keep MAIL_LIVE off + an allowlist so test mail never reaches real users (fleet admins always pass).— components
- mail-live-prod-authPROD load-bearing trap (@broberg/mail 0.3.0+ defaults NOT-live): set RESEND_API_KEY but FORGET MAIL_LIVE=true → every send to a non-admin recipient is SILENTLY skipped, returning {ok:true,skipped:true} → it LOOKS green while real users never get the mail. On an auth path (magic-link / verify / reset) that's a broken login that passes every check. Two musts: (1) MAIL_LIVE=true as a prod secret; (2) treat skipped as a HARD error on auth mail (if(!r.ok||r.skipped) throw) + seal it with a RED test. Bit cms + cardmem.— components
- gotcharesend.batch.send strips attachments — send per-recipient when you embed inline cid: images.— sanne
- webhooksWire the Resend webhook (Svix-signed) for delivered/opened/bounced/complained events.— sanne
- restricted-keyA send-only (restricted) API key 401s on GET /domains ({restricted_api_key}) — you CANNOT list verified domains with it. Check the dashboard then Domains, or just send: HTTP 200 from POST /emails confirms the From domain is verified.— fdaa
- verified-sender-envKeep the sender in an env var (RESEND_FROM), never hardcoded — a later domain switch (after SPF+DKIM+DMARC) is one secret-flip, zero code change.— trail
Stripe7
- framesHOSTED checkout (checkout.stripe.com) renders card fields in the TOP-FRAME, not PCI-iframes — do NOT use @frame. @frame-chain is only for EMBEDDED Stripe Elements on your own page.— sanne
- lensLens E2E primitives (lens_run_flow): clickSelector, fillSelector (CSS + value + optional frame), waitForUrl (redirect-back assert), inspect (CSP-safe DOM dump for selector discovery). No js-eval step exists.— sanne
- selectorsHosted da-locale selector set: pick card via the ROW [data-testid='card-accordion-item'] (NOT -button/-radio, which are 'not visible'); fields input[name='cardNumber'] · input[name='cardExpiry'] (value '12 / 34') · input[name='cardCvc'] · input[name='billingName']; pay [data-testid='hosted-payment-submit-button']; assert waitForUrl '/shop/receipt/'.— sanne
- shippingPhysical goods (shipping_address_collection): input[name='shippingName'|'shippingAddressLine1'|'shippingAddressLine2'|'shippingPostalCode'|'shippingLocality'] + select[name='shippingCountry'] (DK default). cardUseShippingAsBilling is usually checked → billingName hidden; fill shipping only.— sanne
- link-otpGotcha — Stripe Link: an email already known to Link shows an OTP 'confirm it's you' instead of the card form. Use fresh plus-addresses (cb+testN@domain) → no Link prompt.— sanne
- accordionGotcha — accordion: with multiple methods enabled (Card + Klarna) the card fields stay COLLAPSED until 'Kort'/'Card' is selected. Click the card row first.— sanne
- verifyVerify post-payment via sk_test: GET /v1/checkout/sessions?expand[]=data.payment_intent — application_fee_amount is ONLY present on a PAID session (on an open one payment_intent=null). commission / transfer_data / amount_total / shipping_address_collection all readable.— sanne
Supabase8
- regionProvision in region arn (Stockholm) — same as Fly.— components
- lensAuthed Lens capture → @broberg/lens; keep only your signInWithPassword in createSession, package owns the rest.— components
- gotchaCookie-domain trap: behind a proxy the Host header is 'localhost' → cookie never reaches the real domain. Pin LENS_COOKIE_DOMAIN.— fds
- securityservice_role key is server-side ONLY — never ship it to the browser. Use a read-only/anon key client-side.— components
- auth-scannerEmail-security scanners (Outlook SafeLinks, Mimecast) PRE-FETCH confirmation/invite/recovery links, so the token is consumed on the scanner's GET before the user clicks and the user's click then fails ('link broken'). Fix: Click-to-Verify — GET renders a button page (consumes nothing); a POST consumes the token only on a real user click. Scanners only follow GET.— fds
- rls-observabilityRLS silently drops pre-login audit events: events that fire before login (signup-fail, scanner-detected, verification-failed) hit an INSERT policy requiring auth.uid() IS NOT NULL, get rejected, and a swallowing catch hides it = zero history. Use a service-role admin client for legitimate unauth events + replace the silent catch with explicit console.error.— fds
- grantsSupabase removes auto-grants for new tables (Oct 30 2026). Always add explicit GRANT … TO service_role, authenticated (anon only if needed). SECURITY DEFINER fns: SET search_path = public, pg_catalog + REVOKE EXECUTE FROM anon, authenticated unless it IS an RPC.— fds
- ssr-cookie-proxy@supabase/ssr cookie behind a proxy: sb-<ref>-auth-token domain is derived from request Host; behind Apache/nginx/Fly that can be 'localhost'/'0.0.0.0' so the browser NEVER sends the cookie (silent false-green). Pin the cookie domain. Bonus: the cookie SPLITS into .0/.1 chunks when large — handle as an array.— fds
Turso / libSQL5
- reuseConsume via @broberg/db-sdk (libSQL transport) rather than a bespoke client.— components
- regionPrimary DB in arn; add embedded replicas for fast multi-region reads.— components
- fitRight tool when state outgrows a per-machine Fly volume but doesn't need full Postgres.— components
- migration-not-appliedA drizzle migration recorded in __drizzle_migrations is NOT proof the DDL landed. Verify BOTH the hash in the migrations table AND the actual effect (SELECT name FROM pragma_table_info('t') WHERE name='col'). A green migrate can leave the column absent.— trail
- drizzle-gotchadb.update().set() on bun:sqlite can SILENTLY drop a new column (value-independent, while sibling writes land) — workaround: a raw SQL UPDATE. Verify DB ground-truth via flyctl ssh, not the ORM return value.— cardmem
npm / OIDC publishing9
- oidcOIDC + --provenance REQUIRES a repository.url in package.json matching the GitHub repo, else npm 422s. (theme's first OIDC release hit exactly this.)— components
- ciDo NOT set version: on pnpm/action-setup when the root package.json has a packageManager field — they conflict and the publish job fails.— components
- release-gotchagit push --follow-tags only pushes ANNOTATED tags. A lightweight git tag vX won't trigger a tag-gated publish workflow, so the release just doesn't happen. Use git tag -a … or push the tag explicitly (git push origin <tag>).— ai-sdk
- publish-timingRight after publish, npm view / npm i can 404 for minutes — Fastly negative-cache, NOT a failed publish. The publish success line is authoritative; verify npm view <pkg>@<version> before claiming live (each probe re-seeds the negative cache, so don't hammer it).— ai-sdk
- native-dep-isolationIf a package's ROOT entry transitively imports a runtime builtin (bun:sqlite, node:zlib), a BROWSER build hard-fails. Ship a browser-clean subpath export (separate tsup entry + exports['./x']) and PROVE it with bun build --target=browser.— ai-sdk
- first-publishFIRST publish of a brand-new name is chicken-and-egg: npm's Trusted Publisher can't be configured until the package EXISTS, so v0.1.0 must be a token publish that CREATES it. Keep the org publish-token in ONE place (components) and let it bootstrap-publish first versions for the whole fleet — ping components (intercom) when your package is built, rather than copying a publish-everything token into N repos' .env. After v0.1.0 exists, Christian adds the Trusted Publisher and every later release is token-free.— components
- monorepoPublish a @broberg package from a MONOREPO subdir (not its own repo): add a tag-prefixed job (on push tag e.g. complimenta-sdk-v*) with working-directory: packages/<name>, permissions id-token:write, build+test, then `npm publish --provenance`. The Trusted Publisher points at THAT repo + the workflow filename — so one monorepo ships many independently-tagged @broberg packages. (broberg-ai/fdaa → @broberg/complimenta-sdk is the first.)— components
- trusted-publisherTrusted Publisher setup (Christian, ~30s, ONLY after v0.1.0 exists): npmjs.com → the package → Settings → Trusted Publisher → GitHub Actions → Organization + Repository (e.g. broberg-ai/<repo>), Workflow filename (publish.yml), Environment left blank. Then a tag push publishes token-free with provenance — his single manual step per new package.— components
- provenance-privatenpm publish --provenance FAILS for a PRIVATE source repo → npm E422 'Unsupported GitHub Actions source repository visibility: private' (OIDC auth + the signed provenance statement still succeed; only the registry's provenance-verification rejects). Rule: PUBLIC source repo → keep --provenance; PRIVATE repo → omit it. (broberg-ai/fdaa hit this on complimenta-sdk's first OIDC tag-release.)— fdaa
Pitch Vault7
- reuseNeed a customer pitch? Use Pitch Vault, don't roll your own. POST /api/cli/push (multipart, x-api-key) with a self-contained HTML pitch → get a shareUrl back. Search existing pitches first: GET /api/v1/pitches?q=<term> (also ?folderId=).— components
- idempotencySlug = the idempotent UPDATE key. Send the SAME slug to /api/cli/push to overwrite a pitch in-place (there's no separate edit endpoint); omit slug → a new pitch each time. Version via naming (e.g. -v2), not the API.— components
- publishisPublished defaults to FALSE — pass isPublished=true in the push to publish immediately, else the pitch exists but viewer/share links 404.— components
- foldersOrganize via folderId: GET /api/v1/folders first for the tree, then pass folderId in the push (null/omit = root). Folders are created in the web UI, not the API.— components
- self-containedPitch HTML MUST be self-contained — inline <style>, base64 data-URIs for images, NO external CDN/API calls (they fail in the sandboxed viewer). Same F122 rule as our inventory mockups.— components
- generateDon't write from scratch: POST /api/generate (Claude Haiku) turns a brief into a complete self-contained HTML pitch, optionally styled from a template pitch. Real examples live in the repo's pitches/ dir.— components
- delete-and-authNo delete API (web-UI only). To delete programmatically, ask the pitch session via intercom — it has the Fly-volume access. The x-api-key is a Fly secret; never commit it.— components
Image processing (sharp / HEIC)6
- reuseUse @broberg/media-transform for HEIC→JPEG + responsive WebP/JPEG + EXIF orient/strip — don't hand-roll a sharp pipeline per app. transformImage(bytes, {heicToJpeg, keepOriginal, variants:[{name,maxEdge,format,quality}]}) → {variants:[{name,bytes,contentType,width,height}], orientationFixed}. Pipe each variant.bytes into @broberg/media.upload().— components
- heic-hevcsharp CANNOT decode iPhone HEIC: its prebuilt libheif reads the HEIF container + the AVIF decoder but NOT HEVC (sharp.format.heif.input.fileSuffix shows only ['.avif']). sharp(heic).metadata() SUCCEEDS yet .toBuffer() throws 'Decoder plugin error / bad seek'. So a metadata() capability-probe is a false positive — route HEIC through heic-convert (bundles its own HEVC decoder, pure-JS, works on glibc/musl/Bun, applies rotation on decode).— components
- exif-privacyPrivacy: strip EXIF from EVERY output, including the kept original — read any EXIF you need (GPS, capture time) BEFORE transform; never let location survive on a stored/downloadable file. sharp drops metadata by default on encode; .rotate() bakes orientation in and removes the tag.— components
- bunsharp itself loads + runs fine under Bun (verified 0.35 on Bun 1.3) for resize/encode/orient — only the HEVC-HEIC decode needs heic-convert. No wasm/sidecar needed for the rest. Keep sharp/heic-convert as external (native) deps; never bundle them.— components
- test-fixtureGenerate a real HEIC test fixture locally with macOS sips: `sips -s format heic in.jpg --out out.heic` (produces HEVC HEIF). Lets you verify a HEIC decode path with hard runtime proof instead of assuming.— components
- memoryIn-process transform OOMs a small box (512MB) on large photos — sharp/libvips holds the decoded bitmap in RAM. For a batch/backfill, run ONE fresh process per photo (a shell loop) for memory isolation, or bump the machine RAM. A long-lived server transforming one upload at a time is usually fine.— xrt81
GitHub Actions / CI-CD6
- test-gateBlocking test-gate: give the deploy job needs:[test], where test runs the suite (turbo → bun test). One red test → deploy blocked. Closes the hole where a regression ships past 'green-but-never-run' tests.— buddy
- pnpm-setuppnpm/action-setup@v4 fails hard ('Multiple versions of pnpm specified') if you set the version in BOTH the action (with: version:) AND package.json (packageManager: pnpm@x). Fix: drop with:version: entirely, let the action read packageManager.— buddy
- paths-filterA workflow whose paths: filter does NOT include .github/workflows/** does NOT re-trigger when you edit the workflow file itself — so a CI-fix commit 'does nothing' until the next qualifying push. Test a workflow change in isolation with gh workflow run <wf> --ref main (workflow_dispatch).— buddy
- deploy-verifyProve a deploy is really live: match the flyctl releases timestamp against the commit time (release v79 06:13 UTC, 1 min after commit 06:12 = real). A cheap truth-check against a compaction summary that claims 'shipped'.— trail
- probeCheap post-deploy signal: an unauthenticated request returning 401 (not 404) proves the route is mounted + auth-gated without a full authed flow. 404 = not deployed yet; 000 = host not up — never raise a real incident on 000 (false alarm).— cardmem
- autostash-trapgit add → rebase --autostash → commit can DROP your modifications → empty commits → CD rebuilds the OLD code. Verify against the SERVED artifact (bundle hash / content marker), not your working tree.— cardmem
Frontend (Preact / Next / PWA / web)7
- preact-renderPreact ≠ React: NO setState-under-render bailout. Calling setState during render to 'reset' on a prop change makes Preact PAINT the intermediate state first → a visible blink. DERIVE state from an id (openId === curId), never set it during render.— xrt81
- media-flickerThe real 'flicker' in a media viewer is the image RESIZE, not the image swap. In a flex-column layout, if anything under the image changes height on swap (e.g. a lazy detail-fetch blanking the bottom bar) the image grows/shrinks → reads as flicker. Fix: fixed-height bottom bar; read title/date from the LIST row already in memory (not the lazy detail call); float foldable panels in an overlay ABOVE the image so they never touch layout height.— xrt81
- carousel-keyCarousel swap-flash: key your slides (prev|cur|next) on the media id → Preact MOVES the <img> nodes instead of swapping their src (src-thrash = repaint = flash).— xrt81
- ios-fullscreenFullscreen media viewer: use a FULLY opaque layer (position:fixed; inset:0; background:#000; height:100svh) — a translucent scrim lets the app-shell bleed through during transitions. Kill iOS pull-to-refresh with overscroll-behavior:none + touch-action:none + a body-scroll-lock.— xrt81
- gdpr-fontsGDPR-clean webfonts: use fonts.bunny.net, not Google Fonts — Paris-hosted, drop-in compatible with Google Fonts' CSS query API, no visitor data to the US. (On cardmem's mockup allow-list too.)— vn-leker
- lens-touchLens has NO native pinch gesture and synthetic TouchEvents don't reproduce iOS pinch reliably (esp. WebKit) → touch-gesture features (pinch-zoom) CANNOT be auto-verified; say 'device-test required' instead of claiming verified. Prove layout stability instead: assert getBoundingClientRect().height is identical before/after a swipe / panel-open.— xrt81
- next-cache-coherenceNext.js runs MIDDLEWARE and /api/* ROUTE-HANDLERS as SEPARATE module instances → separate module-level state. A `let _cached` in a lib imported by both has TWO copies: a write via the route-handler is INVISIBLE to the middleware's copy until a shared signal re-checks. Symptom (cms, prod): a new site added via a route-handler returned 200 from the API but the middleware router 404'd ALL its pages until restart — looked like a routing bug, was cache-coherence. Fix: any module-level cache backing BOTH middleware AND route-handlers must invalidate on a SHARED signal — file mtime (cheap fs.stat per call) or explicit cross-call invalidation. NEVER cache forever.— cms
AI / LLM providers (@broberg/ai-sdk)4
- capability-probe$0 capability-probe: to check whether a model/capability is actually served in a region/project WITHOUT spending, POST an EMPTY {} body. A 400 INVALID_ARGUMENT 'Empty instances' means the model IS served (it failed validation BEFORE generating → free); a 403 means a setup gate (billing / API not enabled), not a missing capability. Confirmed Veo-3.x in 5 EU regions (europe-west1/3/4/9, europe-north1) + Azure TTS live for $0. Always settle capability cheaply before committing an adapter.— ai-sdk
- gcp-project-trapGCP 'two projects, same display name' trap: AI Studio AUTO-creates a hidden gen-lang-client-XXXX project BEHIND a Gemini API key — distinct from a self-made project with the same display name. Probing the wrong (empty, no-billing) one gives a misleading 403. Run `gcloud projects list` and use the BILLING-enabled project (gen-lang-client- prefix = AI Studio's).— ai-sdk
- tts-routingTTS/speech does NOT go through OpenRouter — it proxies only LLM chat/completions, no speech synthesis. Azure Neural TTS = Azure Speech (Cognitive Services): a separate SSML REST endpoint ({region}.tts.speech.microsoft.com/cognitiveservices/v1, header Ocp-Apim-Subscription-Key). EU-pin the region (swedencentral / westeurope) for GDPR — same discipline as BFL's api.eu. ai.tts is live in @broberg/ai-sdk (Azure adapter for EU neural Danish da-DK).— ai-sdk
- cost-readbackCost read-back > local re-aggregation: Upmetrics OWNS cost aggregation (summary / timeseries / fleet, micro-USD, per-tenant groupBy via its cost read-API). Don't build a local roll-up in a consumer app — write runs to Upmetrics AND read the aggregated cost back from it (ai-sdk's upmetricsCostClient does exactly this). One canonical source, no drift.— ai-sdk
Fleet ops (cc-session auth · edge · inter-session comms)4
- mac-edge-keychainMac-edge keychain trap: a LOCKED macOS login-keychain makes cc fail with 'Not logged in · Please run /login · security unlock-keychain' — and it masquerades as a rate-limit wall, a login bug AND a launcher bug at once (cost 4h to diagnose). Root: when CLAUDE_CODE_OAUTH_TOKEN isn't set in the session env, cc falls back to the keychain cred → a locked/asleep Mac = locked out of auth. Fix (verified on cb-2): `security delete-generic-password -s "Claude Code-credentials"` + run on the env setup-token (CLAUDE_CODE_OAUTH_TOKEN from ~/.buddy/.env). The fleet runs on the setup-token, never the keychain.— buddy
- intercom-voicemailIntercom answering machine (buddy F177): an intercom to a SLEEPING/offline cc-session is no longer lost — it's enqueued durably (survives a buddycloud restart) and flushed on wake, chronologically, exactly-once. Fleet-design consequence: stop checking 'is the peer awake?' before ask_peer/announce — just send; buddy holds it until they wake. Slept ≥90s → it renders as a 'voicemail' card in the recipient's chat.— buddy
- search-historyNew fleet-wide MCP tool: search_history — full-text search (FTS5/BM25) over each edge's 'fermented' layer-2 dialog history, on the buddy channel. Complements trail_search: keyword-hit recall vs. trail's distilled/summarized knowledge. Live in already-running sessions after the next relaunch_fleet; new sessions get it from source automatically.— buddy
- file-pipingNew fleet-wide MCP tool: pipe_file (buddy F200) — move a FILE between edges or up to R2 without the bytes ever passing through a session's context. Two modes: (1) pipe_file({to_edge:'cb-ubuntu', path:'/abs/path.pdf'}) → lands in the target edge's ~/Downloads, sha256-verified both ends, never overwrites (collision → ' (1)' suffix), 25MB inline cap. (2) pipe_file({target:'assets', path:...}) → staged straight to R2 via a hub presigned PUT (NO size cap) → returns a short-TTL URL to hand to cardmem_create_asset({url}). Curl path also exists: POST 127.0.0.1:4123/api/pipe/send. Not an npm package (daemon routes + MCP tool on the buddy channel). Use it instead of base64-ing a file through chat or a one-off scp. Live in running sessions after the next relaunch_fleet.— buddy
Native macOS/iOS apps (Swift — codesign, entitlements, TCC)1
- hardened-runtime-entitlementsHardened Runtime + missing entitlement = SILENT permission denial, no TCC prompt ever shown. Symptom (Trail Ambient, Swift menubar app): AVCaptureDevice.requestAccess(for:.audio) returned with no dialog, no crash, and the app never appeared in System Settings › Privacy › Microphone. Root cause: signed with `codesign --options runtime` (Hardened Runtime) but no `--entitlements` plist — under Hardened Runtime, mic access requires `com.apple.security.device.audio-input` IN THE SIGNATURE ITSELF. `NSMicrophoneUsageDescription` in Info.plist is necessary but NOT sufficient on its own — without the entitlement, macOS denies access before it ever asks. Fix: sign with `codesign --options runtime --entitlements <plist> ...` where the plist sets `com.apple.security.device.audio-input = true` (camera = `com.apple.security.device.camera`; Screen Recording is TCC-only, no entitlement exists for it). ALWAYS verify post-build with `codesign -d --entitlements - <App>` — an empty entitlements block alongside `flags=0x10000(runtime)` in `codesign -d` output is exactly this trap. Cost Trail ~1h to diagnose.— trail