Local-first Electron structured notes application with a NestJS backend and React/Vite frontend.
npm installinstalls all workspace dependencies.
Run the desktop app in three terminals:
npm run dev:backendnpm run dev:frontendnpm run dev:electron
The backend health endpoint is available at /api/health, and Swagger is exposed at /api/docs. The separately deployable content-free relay can be run with npm run dev:notification-service; its protocol and deployment runbook live in notification-service/README.md.
Before running npm run dev:electron, export the public Google and Microsoft OAuth client IDs plus the Google Desktop OAuth client secret in that PowerShell session. See electron/OAUTH.md for provider registration, secure local injection, and packaging details. Microsoft remains a public client without a secret.
npm run buildbuilds the backend, frontend, Electron shell, and notification service.npm run deploy:check:notification-servicevalidates the Cloudflare relay bundle without publishing it.npm run package,npm run package:release, andnpm run package:dirremain backwards-compatible Windows x64 aliases.npm run package:windows,npm run package:windows:release, andnpm run package:windows:dirare the explicit Windows x64 commands.npm run package:mac:x64andnpm run package:mac:arm64create architecture-specific macOS DMG and ZIP artifacts.npm run package:mac:dir:x64andnpm run package:mac:dir:arm64create unpacked architecture-specific Mac application bundles.npm run verify:release-artifacts:windows --workspace electronvalidateslatest.yml, the NSIS installer, its blockmap, size, and hash.npm run verify:release-artifacts:mac --workspace electronvalidates the mergedlatest-mac.yml, both ZIP hashes/sizes, and both DMGs.
Electron packaging stages the backend production dependency closure into electron/.backend-runtime and rebuilds better-sqlite3 for the Electron target architecture. Each Mac architecture must be staged and built in its own clean job; .backend-runtime and packaged directories must never be shared between architectures or with Windows.
The Mac commands can make unsigned local artifacts without release credentials. Those artifacts are development checks only: they must not be uploaded as public releases and do not satisfy Gatekeeper distribution requirements.
The stable desktop release supports:
- Windows x64 through the existing NSIS installer.
- macOS 11.0 or newer on Intel (
x64) and Apple Silicon (arm64).
Mac architectures are built independently. The release contains one DMG and ZIP per architecture and one merged latest-mac.yml. The literal arm64 marker in the Apple Silicon ZIP name is required by the pinned electron-updater 6.8.9 MacUpdater; Intel uses the x64 ZIP fallback. Application identity remains com.cardnotes.app, product name remains NoteStack, updates remain in vojtechportes/card-notes-app, and persisted data remains under the card-notes-app child directory of Electron appData (~/Library/Application Support on macOS).
Publishing a GitHub release triggers .github/workflows/release-electron.yml from one version-update commit. Windows and the two Mac architectures are prepared independently. No platform is uploaded publicly until all jobs have completed signing, notarization, runtime verification, and artifact validation.
The Windows path remains unchanged: build an x64 app directory, sign nested executables through Certum SimplySign, create and sign the NSIS installer, refresh the signed bytes in latest.yml and its blockmap, and validate the complete set.
The Mac matrix uses macos-15-intel for x64 and macos-15 for arm64. Each job rebuilds better-sqlite3, signs the complete .app with a Developer ID Application identity and hardened runtime, notarizes the app with an App Store Connect API key, creates architecture-safe DMG/ZIP artifacts, separately notarizes and staples the DMG, and verifies:
- the app and native SQLite Mach-O architecture;
- the packaged backend dependency closure and health endpoint;
- two concurrent isolated packaged OAuth runs;
- deep/strict code signing, Gatekeeper assessment, and app/DMG stapling;
- DMG mount/copy installation and ZIP application payload integrity.
The final job merges the two updater manifests, validates Windows and macOS artifacts together, removes/replaces only the exact expected asset names for the release version, uploads them, and queries GitHub to prove every expected asset exists exactly once. If upload or post-upload verification fails, it removes only that exact expected set so a rerun has a deterministic recovery path. GitHub uploads are not transactional, so a release must not be announced as available until this final job succeeds.
Expected public assets for version <version> are:
latest.ymlnotestack-<version>-setup.exenotestack-<version>-setup.exe.blockmaplatest-mac.ymlnotestack-<version>-x64.dmgnotestack-<version>-x64.zipnotestack-<version>-arm64.dmgnotestack-<version>-arm64.zip
Required GitHub Actions repository variables:
NOTESTACK_GOOGLE_OAUTH_CLIENT_ID: public Google Desktop OAuth client ID.NOTESTACK_MICROSOFT_OAUTH_CLIENT_ID: public Microsoft native/public application client ID.
Required shared application secret:
NOTESTACK_GOOGLE_OAUTH_CLIENT_SECRET: Google Desktop OAuth client secret. The packaged value is extractable and is not an application-identity security boundary.
Required Windows signing secrets:
CERTUM_USER_IDCERTUM_OTP_URICERTUM_CERT_FINGERPRINT
Required Mac signing/notarization secrets:
MACOS_CERTIFICATE_BASE64: base64-encoded Developer ID Application.p12certificate and private key.MACOS_CERTIFICATE_PASSWORD: password for that.p12.APPLE_API_KEY_BASE64: base64-encoded App Store Connect.p8API key.APPLE_API_KEY_ID: App Store Connect API key ID.APPLE_API_ISSUER: App Store Connect issuer ID.
GITHUB_TOKEN is supplied by Actions for artifact publishing. Apple credentials are scoped to the Mac build job, materialized only below RUNNER_TEMP, permission-restricted, and never printed. Do not place certificate or API-key contents in repository files, logs, release artifacts, or support bundles.
The Windows signing job uses ghcr.io/reactiveui/certum-signer:latest. It must expose SS_DIST, JSIGN_JAR, and osslsigncode. The Certum publisher name in electron/scripts/write-app-update-publisher.mjs remains Windows-only; macOS updater verification relies on the signed application update path.
- Configure the OAuth, Certum, Developer ID Application, and App Store Connect credentials.
- Run
npm test,npm run lint,npm run build, and the available local platform packaging checks. - Create two private/draft fixture releases: an older signed/notarized version and a newer candidate. Retain them until both update rehearsals pass, then delete their assets and tags explicitly.
- On clean standard-user Intel and Apple Silicon Macs, install the older DMG without a Gatekeeper bypass. Verify first launch, quit/reopen, Dock reactivation, native traffic-light controls, note CRUD, images, import/export, persistence, OAuth, sync startup, and external links.
- Update in-app to the newer version and repeat the checks. Confirm
latest-mac.ymlselects the x64 ZIP on Intel and thearm64ZIP on Apple Silicon. - Run
codesign --verify --deep --strict,spctl --assess,xcrun stapler validate, and notarization-history/status checks against the candidate artifacts. - Repeat the existing Windows clean install and older-to-newer update smoke test.
- Publish the real release, wait for
publish-electron-releaseto succeed, and verify the eight exact assets above before announcing availability.
Record the workflow run URL, release URL, x64/arm64 machine and OS versions, notarization submission IDs, clean-install results, update rehearsal versions, Windows regression result, tester, and date in the release notes. TMSC-42 is not operationally complete until this evidence exists.
- Missing-secret failures name only the absent variable. Confirm repository secrets are configured; never echo their values.
- A signing identity error usually means the
.p12is not a Developer ID Application identity, its password is wrong, or the certificate has expired. - A notarization failure should be investigated with the submission ID and
xcrun notarytool log; inspect diagnostics for unsigned nested binaries or rejected entitlements without posting credentials. - A wrong-architecture SQLite error means
.backend-runtimewas reused ornpm_config_archdid not match the runner. Delete the staged runtime, rebuild on the matching runner, and confirm withlipo -archs. - If the wrong Mac update is chosen, confirm the merged manifest has exactly two ZIP entries and that only the Apple Silicon filename contains the literal
arm64marker. - If publishing fails, rerun the workflow after correcting the cause. The publisher removes only the exact expected version asset set; do not broadly delete release assets.
- SimplySign Desktop has no stable headless API. Windows signing can remain sensitive to UI timing, token state, TOTP clock drift, and signer-image updates.
- Newly issued Windows certificates can require reputation history before SmartScreen warnings disappear.