* enhance: use graph identity in URLs * fix: resolve graph identity URLs * fix: satisfy graph url lint * fix: resolve graph id from hash urls * fix: preserve tab graph on reload * fix: initialize tab graph before render * enhance: open graph in another tab * fix: open graph tabs by graph id * fix: open local graph tabs by registry id * fix: avoid duplicate rtc graph id * fix: open electron graph window on shift click * fix: address graph identity review
9.9 KiB
ADR 0017: URL Target Graph Resolution
Date: 2026-05-20 Status: Accepted
Context
Logseq URLs can point at a graph, page, block, or file. Today the graph target is not carried through one coherent flow.
The custom protocol path is handled in Electron main:
logseq://graph/<graph>andlogseq://new-window/<graph>are parsed inelectron.url/local-url-handler.- Electron resolves
<graph>throughelectron.handler/get-graph-name. - If no window is already associated with that graph, Electron asks the current
renderer to run
openNewWindowOfGraph. - The renderer then opens a new Electron window with
#/?graph=<repo>.
This depends on renderer IPC timing and renderer-local storage. On first app
launch, the initial deeplink is handled soon after the main window is created,
while the renderer listener can still be unavailable. When the message is lost,
the newly opened app falls back to the stored :git/current-repo, so the URL
appears to always open the current graph.
The web app has a related weakness. frontend.state only reads :graph from
frontend.util/parse-params during initial state creation, and that parser only
understands hash fragments shaped like #/?graph=.... Page/block web URLs and
later hash changes can route inside the currently opened graph instead of first
switching to the graph named by the URL.
Graph names are not stable identifiers. Users can rename graphs, and two linked graphs can share the same short basename. URLs that are meant to survive renames, browser sessions, and device handoffs need to identify the graph by id.
Decision
Make URL graph selection an explicit target object and process it before route navigation in the renderer, with a small persisted graph registry used to turn stable graph ids into local repos.
- Introduce a shared URL target model for Electron protocol URLs, web URLs, and
mobile URLs:
:graph-id- canonical graph id from the URL.:graph-identifier- compatibility alias, such as a short graph name or full DB repo name.:repo- resolved full local repo name, for examplelogseq_db_work.:route- one of graph home, page, block, or file.:open-mode- current window or new window.:source- protocol URL, web URL, startup argv, or second instance.
- Use
:graph-idas the canonical URL identity. For remote graphs,:graph-idis the remote graph UUID. For local-only graphs,:graph-idis the local graph UUID. Generated URLs should includegraph-idwhenever the graph id is known. - Resolve URL graph targets through one graph resolver that accepts, in order:
- graph ids from the local graph registry;
- full DB repo names, such as
logseq_db_work; - short local graph names, such as
work; - existing canonicalized DB repo names.
- Treat an unresolved graph id as an unresolved URL target, not as a successful match for the current graph. Mobile reports this to the user. Web startup currently logs the unresolved target and continues with the tab-local or stored graph so the app can still boot.
- For Electron custom protocol URLs, the main process owns URL intake and graph-name/graph-id resolution. This branch keeps the existing renderer-mediated window creation path for custom protocol URLs and teaches it to resolve graph ids through the registry.
- For web and mobile URLs, the renderer owns URL intake and graph resolution. It must switch to the target graph before applying the route target.
- Once the target graph is loaded, apply the route target exactly once.
- Do not store URL graph targets in
:git/current-repountil the graph switch succeeds. - Use
sessionStoragefor tab-local graph identity on web reloads that do not carry a URL graph target. This preserves each browser tab's graph without appendinggraph-idto every internal route.
Supported URL Shapes
Canonical generated web URLs use graph ids:
https://logseq.com/?graph-id=<graph-uuid>https://logseq.com/page/<page-uuid>?graph-id=<graph-uuid>https://logseq.com/block/<block-uuid>?graph-id=<graph-uuid>
Electron protocol URLs accept graph ids through the same graph identifier slot:
logseq://graph/<graph-uuid>logseq://graph/<graph-uuid>?page=<page-name-or-uuid>logseq://graph/<graph-uuid>?block-id=<block-uuid>logseq://new-window/<graph-uuid>?page=<page-name-or-uuid>
Compatibility URLs remain accepted:
logseq://graph/<graph-name>logseq://graph/<graph-name>?page=<page-name>logseq://graph/<graph-name>?block-id=<uuid>logseq://graph/<graph-name>?file=<file>https://logseq.com/#/?graph=<graph-name>https://logseq.com/#/page/<page-name>?graph=<graph-name>
Generated web URLs should use path routes instead of hash routes. The app may
continue accepting hash-routed URLs during migration, but new share/copy-link
helpers should emit path URLs with graph-id.
Graph Registry
URL graph-id resolution must not open every graph database during startup. Keep a small graph registry outside each graph DB.
- Web and mobile store the graph registry in IndexedDB.
- Electron stores the graph registry in
(.getPath app "home")/.logseq/graphs.edn. - Registry entries include at least:
:graph-id:repo:graph-name:local-graph-id:updated-at
- Do not persist a separate
:rtc-graph-idin the registry. For remote graphs,:graph-idis the remote graph UUID. For local-only graphs,:graph-idis the local graph UUID. - Update the registry whenever a graph is created, opened, imported, downloaded, uploaded to RTC, renamed, unlinked, or deleted.
- If a registry lookup fails for a graph id, the app may run an explicit repair path that enumerates local graph storage and opens graph DBs to rebuild the registry. This repair path must not be the normal URL-open path.
Electron Runtime Design
- Store the registry at
(.getPath app "home")/.logseq/graphs.edn. - Resolve Electron protocol graph identifiers through that registry before falling back to existing repo/name lookup.
- For shift-click in the graph list, open a new Electron window through the
existing
openNewWindowIPC using the resolved repo. Electron windows still use repo identity for window creation. - Keep a per-window pending target in Electron state only for work that cannot
be represented safely in the URL, such as a file redirect that requires graph
readiness. Avoid a single global
:window/once-graph-readycallback because concurrent windows can overwrite each other. - When the renderer reports
graphReady, Electron should look up and consume the pending target for that specific window id. - Existing error notifications for missing or unknown graphs remain, but they should be sent to the selected/fallback window after graph resolution fails.
- Direct main-process creation of route-aware graph windows remains follow-up
work; it should replace the renderer-mediated
openNewWindowOfGraphpath.
Web Runtime Design
- Add a parser that can read graph targets from path URLs and compatibility hash URLs.
- During frontend startup:
- parse the URL target;
- resolve
:graph-idthrough the IndexedDB graph registry; - resolve compatibility
:graph-identifieragainst the known graph list when no:graph-idis present; - initialize from the resolved URL target when it resolves;
- otherwise use the tab-local
sessionStoragegraph target, then the stored:git/current-repo, then the first linked repo.
- During startup, apply resolved page/block URL targets after the target graph is restored. Later in-app route changes with graph targets are follow-up work.
- During route generation:
- generated page routes include
graph-idwhen the current graph id is available, but startup/render-phase redirects tolerate a temporarily unavailable graph id instead of crashing.
- generated page routes include
- Generated web links from
frontend.util.urlshould includegraph-idon the actual route. - Web "open in another tab" and shift-click from All graphs open
#/?graph-id=URLs for existing local graphs.
Mobile Runtime Design
- Use the same URL target parser and resolver as the web runtime.
- Store and resolve the graph registry from IndexedDB.
- Treat mobile app intents and universal links as URL sources that produce the same target model.
Consequences
- Resolvable graph-id URLs open the requested graph instead of silently using the last current graph.
- Electron protocol URLs can resolve graph ids via the registry, but route-aware direct window creation remains follow-up work.
- Web graph URLs become route-aware, so page and block links can switch graph before navigating during startup and mobile deeplink handling.
- Web and mobile graph-id resolution is fast because it reads the graph registry instead of scanning OPFS graph directories and opening graph databases.
- Electron graph-id resolution is fast because it reads
(.getPath app "home")/.logseq/graphs.edninstead of opening each graph database. - Bare web reloads preserve the tab's graph through
sessionStorage, so two tabs can remain on different graphs even when one tab changes the global current graph. - The implementation needs focused tests around URL parsing, graph resolution, first-launch Electron deeplinks, second-instance deeplinks, and web/mobile route changes.
Follow-up Work
- Add Electron main tests for first-launch and second-instance deeplinks.
- Add renderer tests proving that web URLs switch graph before page/block navigation after graph switches triggered by later route changes, not only startup URLs.
- Add graph registry read/write tests for IndexedDB and the Electron
(.getPath app "home")/.logseq/graphs.ednregistry. - Replace
:window/once-graph-readywith per-window pending targets. - Replace Electron custom protocol's renderer-mediated
openNewWindowOfGraphpath with direct main-process route-aware window creation.