Common Gotchas
Issues you'll hit while working on memrynote, and the canonical fixes.
better-sqlite3 NODE_MODULE_VERSION Mismatch
Symptom: ERR_DLOPEN_FAILED, NODE_MODULE_VERSION X but expecting Y.
Two fix paths depending on the target:
| Target | Fix |
|---|---|
| Node tests | pnpm rebuild better-sqlite3 (or bash apps/desktop/scripts/ensure-native.sh node) |
| Electron app / E2E | bash apps/desktop/scripts/ensure-native.sh electron (or pnpm rebuild:electron) |
Using the Node fix for Electron leaves
autoOpenLastVaultsilently failing withERR_DLOPEN_FAILED. The app never opens the test vault, and E2E waits for workspace surfaces time out.
Zod v4
z.record(z.unknown()) throws in safeParse under Zod v4. Use:
z.record(z.string(), z.unknown())This caught Phase 3 sync schemas — a few places still use the old form on legacy branches.
Drizzle Nullable JSON Columns
Drizzle's .values() insert distinguishes null from undefined. For nullable JSON columns, pass null explicitly:
db.insert(tasks).values({
id,
fieldClocks: null, // ← required for nullable JSON
...
})Passing undefined produces an INSERT that omits the column, then SQLite errors on NOT NULL columns or returns wrong rows.
Migrations Are Hand-Written Since 0020
pnpm db:generate proposes unrelated renames because Drizzle's meta snapshots stop at 0020. Hand-write the SQL and journal entry instead of running the generator.
Workflow:
- Update the schema in
packages/db-schema. - Add a new migration file (
migrations/00xx_description.sql). - Append a journal entry in
migrations/meta/_journal.json. - Run
pnpm db:pushto apply.
Submit Buttons That Disable Mid-Click
If onClick calls a handler that synchronously sets state which adds disabled={isSubmitting} to the button, the browser suppresses the click between pointerdown and click. The user thinks they clicked but nothing happens.
Fix: fire submit from onPointerDown. Keep onClick as a keyboard-activation fallback:
<button
onPointerDown={() => void submit()}
onClick={() => void submit()} // keyboard / accessibility fallback
disabled={isSubmitting}
>
Save
</button>See calendar-quick-create-dialog.tsx for the canonical version.
Editor-Zone Mousedown Handlers Steal Focus from BlockNote Menus
BlockNote's shadcn menus (drag-handle menu, side menu, toolbars and their nested dropdowns) render inline inside .bn-container, not portaled. Any editor-zone mousedown handler — such as the "click the marquee zone to focus the editor at end" handler in note.tsx / journal.tsx, or the marquee selection hook — therefore also sees clicks on menu items. If such a handler focuses the editor on mousedown, the menu unmounts between pointerdown and pointerup, so the item's click never lands and the action silently does nothing (for example, drag-handle Colors/Delete appear to do nothing).
Fix: bail before touching focus when the target is inside menu UI:
if (
target.closest(
'.bn-side-menu, .bn-formatting-toolbar, .bn-suggestion-menu, .bn-link-toolbar, .bn-drag-handle-menu, .bn-menu-dropdown, [role="menu"]'
)
)
returnThis mirrors shouldStartMarquee in use-block-marquee-selection.ts. Regression coverage: tests/e2e/editor-drag-handle-menu.e2e.ts.
Cross-Platform Env Vars in package Scripts
VAR=value cmd is POSIX-only. pnpm runs package scripts through cmd on Windows, where MEMRY_ENV=production pnpm ... fails with 'MEMRY_ENV' is not recognized. This broke the Windows release build (apps/desktop build script). Use cross-env for any inline env var that must work on Windows too:
"build": "cross-env MEMRY_ENV=production pnpm typecheck && cross-env MEMRY_ENV=production electron-vite build"The macOS and Linux release builds run scripts via sh, so the bug only surfaces in the Windows release job.
Lazy URL Resolution in http-client
The HTTP client resolves URLs per-call, not at module-import time. This avoids tests crashing on import when env vars are absent. If you add a new client, follow the same pattern: read env inside the function, not in module scope.
Pre-Existing Type Errors
These files have known type errors unrelated to runtime behavior. Ignore them when running pnpm typecheck:
apps/desktop/src/main/sync/websocket.test.tsapps/desktop/src/main/folders/folders.test.tsapps/desktop/src/main/sync/sync-telemetry.ts
For non-contract changes, use pnpm typecheck:node && pnpm typecheck:web to skip the flaky ipc:check pre-hook and the pre-existing sync-telemetry.ts error.
Virtualized UI Tests
@tanstack/react-virtual + jsdom = zero items rendered (because jsdom doesn't compute scroll heights). Cover virtualized calendar, week-view, and long-list UIs at the Playwright E2E layer only.
CRDT Sign-Out / Sign-In Ordering
When working in apps/desktop/src/main/sync/runtime.ts:
engine.start() # pull from server FIRST
└─ seedExistingCrdtDocs() # fire-and-forget, only fills orphansReversing the order causes split brain. See CRDT & Notes Sync for full reasoning.
Logging
Always use createLogger('Scope') from electron-log — never console.*. A pre-commit hook flags raw console.* calls.
DevTools Startup
The desktop app does not open DevTools automatically in development or production. Open them manually from the View menu or with the Electron DevTools shortcut when debugging startup.
User-Facing Errors
Always strip Electron IPC noise from error messages before display:
import { extractErrorMessage } from '@/lib/ipc-error'
toast.error(extractErrorMessage(err, 'Could not save note'))RTL-Safe Tailwind
New code must use logical properties (ms-*, pe-*, start-*, text-start, border-s, rounded-s-*) instead of physical ones (ml-*, pr-*, left-*, text-left, border-l, rounded-l-*). The lint config allows physical classes only in pre-existing files.
The staged renderer guard scans whole staged renderer files, not just new hunks. If you touch a file that still has physical direction classes, convert those nearby classes to logical equivalents before committing.
Security Scan Patterns
GitHub code scanning and the local staged-secret hook are intentionally conservative. When fixing or adding security-sensitive code:
- Compare URL hosts through
new URL(...).hostnameor a DOM anchor fallback, notstring.includes(). - Write generated files and vault payloads through exclusive temporary files plus
rename, not predictable temp paths. - Use
mkdtempfor tests that need temporary directories. - Keep log output sanitized. Do not print signing paths, API responses with headers, or raw error objects that may include request data.
- Invoke package-manager CLIs through a resolved Node/Corepack entry point instead of relying on
PATH. - For generated TypeScript, prefer data tables plus runtime assembly over interpolating dynamic keys into code snippets.
- In fixtures, avoid object fields named
token,secret, orapiKeywhen the value is runtime data. Use a neutral field name and keep the real header name only at the request boundary.
Pre-Production Database
memrynote is pre-production and the DB schema is resettable. There are no backward-compat constraints on schema changes within the desktop app. If a migration is messy, deleting the local vault is a valid recovery.