Skip to content

The stable and nightly Mac apps run at the same time and share one database #517

Description

@Jacksondr5

What happened

Reported by Jackson on 2026-10-10, testing 0.0.49-nightly.20261010.2.

With the stable Mac app (0.0.48, "J5 Code") running, he opened the nightly app ("J5 Code (Nightly)"). Expected, from reading the code: the second app finds the single-instance lock taken, quits, and the running app's window comes forward.

Actual:

  • The nightly launched alongside the stable app. No window was focused and nothing quit.
  • The nightly's bundled server migrated the local database (~/.j5code on the Mac) while the stable app was still running against it.
  • The nightly could not connect to his server, which is still on 0.0.48. That part is expected: a client from after the ledger re-key refuses an older J5 server (register D29).
  • He rolled the Mac back by hand.

So two builds had one profile and one database open, and the older build was left running on a database the newer one had migrated. That is the state the packaging runbook says must never happen.

Why it was expected to be prevented

apps/desktop/src/app/DesktopClerk.ts sets Electron's userData path and then creates upstream's Clerk SDK bridge, which per the comments there "acquires Electron's profile-scoped single-instance lock"; a secondary instance sees bridge.isPrimaryInstance === false and quits. Stable and nightly both resolve the same profile (j5code, apps/desktop/src/app/DesktopUserData.ts) and share the bundle id codes.jackson.j5code. That reading was wrong in practice. It was never tested across the two bundles.

Leads, none verified

  • Whether the lock is taken at all in a packaged macOS build, and what it is keyed on: the userData path, the app name, or something the bridge derives from stateDir. In feat(j5): advance to upstream main at 29980a3140 #500 the Electron app name became the display name without parentheses ("J5 Code", "J5 Code Nightly"), so the two apps have different names; if the lock or the profile path follows the app name anywhere, they would not collide.
  • Whether resolveUserDataPath gives both apps the same path on his Mac. It returns the legacy J5 Code directory when that exists, otherwise j5code; check what each app actually used.
  • Order of startup: whether the bundled server starts, and so snapshots and migrates, before the single-instance check can stop the app. Even a working lock is too late if the backend has already opened the database.
  • Whether upstream's stable and nightly apps have the same hole. They also share one profile (t3code-v2). If so this is worth offering upstream (Upstream give-back backlog (hold until V2 merges upstream) #276).

What a fix has to guarantee

  • A second J5 Code desktop app, of either channel, does not start its bundled server while another is running on the same home. The check has to come before anything opens the database.
  • The person is told why the app they opened did not start, and which app is already running.
  • Separately, and possibly out of scope here: a Mac app and a j5 background service sharing one home on the same machine have the same conflict.

Until then

Quit the stable app before opening the nightly. The nightly release notes and docs/j5/runbooks/macos-packaging.md say so, but nothing enforces it.

Activity

  1. Jacksondr5 commented on Oct 10, 2026

    @Jacksondr5
    OwnerAuthor

    Investigation (2026-10-10)

    Why nothing stopped the second app. J5 never takes Electron's single-instance lock itself. apps/desktop/src/app/DesktopClerk.ts relies on the Clerk SDK bridge to take it, and @clerk/electron skips the lock on macOS by design: its README says "macOS is unaffected ... no lock is taken and isPrimaryInstance is always true". So on a Mac the quit at DesktopClerk.ts:136 never fires. The comments there that say the bridge takes the lock are wrong for macOS.

    Nothing else catches it either:

    • The server's "already running for this home" preflight (apps/server/src/cli/config.ts:335) only applies in web mode. The Mac app starts its server in desktop mode.
    • SQLite opens in WAL mode with a busy timeout, so two servers can open the database together.
    • DesktopClerk.test.ts mocks the bridge, so no test exercised a real lock.

    The leads above.

    Decision (Jackson, 2026-10-10): wait for upstream. Upstream PR pingdotgg#16102 enforces one owner per home in the server: an exclusive lock taken before the database opens, a refusal with exit code 78, and a dialog in the desktop app. It covers the stable and nightly apps, and the Mac app beside a j5 service. J5 will take it through an upstream sync instead of building its own guard.

    Until then the rule is manual: quit the stable app before opening the nightly, and do not run the Mac app beside a j5 service on the same home. The pre-migration snapshot is still taken if that goes wrong.

    Related. #518 covers the other half: an older build starting on a database a newer build migrated. It follows pingdotgg#16102, because it reuses that PR's exit-78 dialog in the Mac app.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions