diff --git a/CHANGELOG.md b/CHANGELOG.md
index 42073477526..e371ed4579e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv
### Added
+- We added `jabref://` protocol handler registration so the browser extension can launch or foreground JabRef when the HTTP server is unreachable. [#15378](https://github.com/JabRef/jabref/pull/15378)
- We added support for downloading full-text PDFs from Wiley journals via the Wiley TDM API. [#13404](https://github.com/JabRef/jabref/issues/13404)
- We added `--key-patterns` option to CLI parameters to allows users to set a citation key's pattern for a specific entry type. [#14707](https://github.com/JabRef/jabref/issues/14707)
- We added a CLI option `--field-formatters` to the `convert` and `generate-bib-from-aux` commands to apply field formatters during export. [#11520](https://github.com/JabRef/jabref/issues/11520)
diff --git a/docs/decisions/0055-browser-extension-communication-architecture.md b/docs/decisions/0055-browser-extension-communication-architecture.md
new file mode 100644
index 00000000000..6ed74a5ffc4
--- /dev/null
+++ b/docs/decisions/0055-browser-extension-communication-architecture.md
@@ -0,0 +1,97 @@
+---
+nav_order: 55
+parent: Decision Records
+status: proposed
+date: 2026-03-06
+---
+# Use Hybrid Architecture (Protocol Handler + HTTP) for Browser Extension Communication
+
+## Context and Problem Statement
+
+JabRef's browser extension imports bibliographic data from web pages into the desktop application via HTTP requests to a local server (`jabsrv`).
+If JabRef is not running, the request fails silently and the import is lost.
+Additionally, the server sets `Access-Control-Allow-Origin: *` without authentication, allowing any website to send requests.
+
+How should the extension communicate with the desktop application to solve both problems while remaining cross-platform, cross-browser, and maintainable?
+
+## Decision Drivers
+
+* Must work on Windows, macOS, and Linux
+* Must work in Chrome and Firefox under Manifest V3
+* Must handle the case when JabRef is not running
+* Must authenticate the extension and protect against CSRF
+* Changes must be maintainable by JabRef's open-source community
+
+## Considered Options
+
+* Native Messaging
+* Local HTTP API (status quo)
+* Protocol Handler only
+* Hybrid — Protocol Handler + HTTP
+* WebSocket
+* Companion / Daemon
+
+## Decision Outcome
+
+Chosen option: "Hybrid — Protocol Handler + HTTP", because it comes out best (see below).
+
+The extension sends data via HTTP to `jabsrv` on localhost.
+When JabRef is not running, a `jabref://` protocol handler starts the application; the extension then polls until the HTTP endpoint becomes reachable.
+
+### Consequences
+
+* Good, because HTTP is platform- and browser-independent using standard `fetch()` API
+* Good, because the protocol handler starts JabRef when it is not running, without encoding data in the URL
+* Good, because `jabsrv` already provides the HTTP infrastructure
+* Good, because graceful degradation to pure HTTP when handler is not registered
+* Bad, because a localhost listener requires CSRF mitigations (custom headers, origin checks)
+* Bad, because the protocol handler must be registered on three operating systems
+* Bad, because two communication channels increase implementation complexity
+
+## Pros and Cons of the Options
+
+### Native Messaging
+
+* Good, because no network listener exposed — best security properties
+* Good, because the browser manages the host process lifecycle
+* Bad, because six manifest variants and two host script languages required
+* Bad, because high packaging overhead across multiple OS and package formats
+* Bad, because JabRef already aims to remove this infrastructure due to maintenance burden (see PR #14884)
+
+### Local HTTP API (status quo)
+
+* Good, because platform- and browser-independent, easy to test
+* Bad, because no mechanism to start JabRef when not running — import is lost
+
+### Protocol Handler only
+
+* Good, because can launch JabRef when not running
+* Bad, because no authentication possible — any website can trigger the URL
+* Bad, because limited URL length
+* Bad, because unidirectional, no response channel
+
+### Hybrid (Protocol Handler + HTTP)
+
+* Good, because HTTP handles data transfer with full response channel
+* Good, because protocol handler carries no data, avoiding the security issues of "Protocol Handler only"
+* Good, because REST endpoints are extensible for future features
+* Bad, because localhost listener requires CSRF mitigations
+* Bad, because two communication channels increase complexity
+
+### WebSocket
+
+* Good, because persistent bidirectional connection with low latency
+* Bad, because no mechanism to start JabRef when not running
+* Bad, because WebSocket is not subject to Same-Origin Policy — any website can connect
+
+### Companion / Daemon
+
+* Good, because handles the offline-app case via local queue
+* Bad, because three fundamentally different service managers per OS
+* Bad, because effectively a second software project with own build system and CI/CD
+
+## More Information
+
+* [Issue #17: Architecture discussion](https://github.com/JabRef/JabRef-Browser-Extension-fresh/issues/17)
+* [PR #18: Protocol Handler PoC](https://github.com/JabRef/JabRef-Browser-Extension-fresh/pull/18)
+* [PR #14884: Remove Native Messaging infrastructure](https://github.com/JabRef/jabref/pull/14884)
diff --git a/flatpak/org.jabref.jabref.desktop b/flatpak/org.jabref.jabref.desktop
index 839978b0c88..c6081f1981a 100644
--- a/flatpak/org.jabref.jabref.desktop
+++ b/flatpak/org.jabref.jabref.desktop
@@ -9,4 +9,4 @@ Exec=JabRef %U
Keywords=bibtex;biblatex;latex;bibliography
Categories=Office;
StartupWMClass=org-jabref-JabRefMain
-MimeType=text/x-bibtex;
+MimeType=text/x-bibtex;x-scheme-handler/jabref;
diff --git a/jabgui/buildres/linux/JabRef.desktop b/jabgui/buildres/linux/JabRef.desktop
index a5d3afddf18..618dcf14eff 100644
--- a/jabgui/buildres/linux/JabRef.desktop
+++ b/jabgui/buildres/linux/JabRef.desktop
@@ -7,7 +7,7 @@ Icon=APPLICATION_ICON
Terminal=false
Type=Application
Categories=DEPLOY_BUNDLE_CATEGORY
-DESKTOP_MIMES
+MimeType=text/x-bibtex;x-scheme-handler/jabref;
GenericName=BibTeX Editor
Keywords=bibtex;biblatex;latex;bibliography
diff --git a/jabgui/buildres/macos/Info.plist b/jabgui/buildres/macos/Info.plist
index 2c733d1931f..b95c33238f4 100644
--- a/jabgui/buildres/macos/Info.plist
+++ b/jabgui/buildres/macos/Info.plist
@@ -31,6 +31,17 @@
DEPLOY_BUNDLE_CFBUNDLE_VERSION
NSHumanReadableCopyright
DEPLOY_BUNDLE_COPYRIGHTDEPLOY_FILE_ASSOCIATIONS
+ CFBundleURLTypes
+
+
+ CFBundleURLName
+ JabRef Protocol
+ CFBundleURLSchemes
+
+ jabref
+
+
+
NSHighResolutionCapable
true
NSSupportsAutomaticGraphicsSwitching
diff --git a/jabgui/buildres/windows/main.wxs b/jabgui/buildres/windows/main.wxs
index 2bbd99d17f9..362379b87e0 100644
--- a/jabgui/buildres/windows/main.wxs
+++ b/jabgui/buildres/windows/main.wxs
@@ -160,9 +160,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/jabgui/src/main/java/org/jabref/cli/ArgumentProcessor.java b/jabgui/src/main/java/org/jabref/cli/ArgumentProcessor.java
index 7ace2484925..9fbeb38b11b 100644
--- a/jabgui/src/main/java/org/jabref/cli/ArgumentProcessor.java
+++ b/jabgui/src/main/java/org/jabref/cli/ArgumentProcessor.java
@@ -1,6 +1,7 @@
package org.jabref.cli;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import java.util.prefs.BackingStoreException;
@@ -18,6 +19,7 @@
public class ArgumentProcessor {
private static final Logger LOGGER = LoggerFactory.getLogger(ArgumentProcessor.class);
+ private static final String JABREF_PROTOCOL_SCHEME = "jabref:";
public enum Mode { INITIAL_START, REMOTE_START }
@@ -28,6 +30,7 @@ public enum Mode { INITIAL_START, REMOTE_START }
private final List uiCommands = new ArrayList<>();
private boolean guiNeeded = true;
+ private final boolean protocolHandlerInvoked;
public ArgumentProcessor(String[] args,
Mode startupMode,
@@ -36,8 +39,24 @@ public ArgumentProcessor(String[] args,
this.preferences = preferences;
this.guiCli = new GuiCommandLine();
+ String[] filteredArgs = filterProtocolHandlerArgs(args);
+ this.protocolHandlerInvoked = filteredArgs.length < args.length;
+
cli = new CommandLine(this.guiCli);
- cli.parseArgs(args);
+ cli.parseArgs(filteredArgs);
+ }
+
+ /// Removes `jabref://` protocol handler URLs from the argument list.
+ /// These are passed by the OS when the `jabref://` URL scheme is triggered
+ /// and must not reach picocli, which would try to parse them as file paths.
+ private static String[] filterProtocolHandlerArgs(String[] args) {
+ return Arrays.stream(args)
+ .filter(arg -> !isJabRefProtocolArgument(arg))
+ .toArray(String[]::new);
+ }
+
+ private static boolean isJabRefProtocolArgument(String arg) {
+ return arg.regionMatches(true, 0, JABREF_PROTOCOL_SCHEME, 0, JABREF_PROTOCOL_SCHEME.length());
}
public List processArguments() {
@@ -94,6 +113,11 @@ public List processArguments() {
if (guiCli.importBibtex != null) {
uiCommands.add(new UiCommand.AppendBibTeXToCurrentLibrary(guiCli.importBibtex));
}
+
+ if (protocolHandlerInvoked) {
+ uiCommands.add(new UiCommand.Focus());
+ }
+
return uiCommands;
}
diff --git a/jabgui/src/main/java/org/jabref/gui/JabRefGUI.java b/jabgui/src/main/java/org/jabref/gui/JabRefGUI.java
index dac75765918..83129deae9b 100644
--- a/jabgui/src/main/java/org/jabref/gui/JabRefGUI.java
+++ b/jabgui/src/main/java/org/jabref/gui/JabRefGUI.java
@@ -20,6 +20,7 @@
import javafx.stage.WindowEvent;
import org.jabref.gui.clipboard.ClipBoardManager;
+import org.jabref.gui.desktop.os.NativeDesktop;
import org.jabref.gui.frame.JabRefFrame;
import org.jabref.gui.help.VersionWorker;
import org.jabref.gui.icon.IconTheme;
@@ -469,6 +470,11 @@ public void startBackgroundTasks() {
if (remotePreferences.enableLanguageServer()) {
languageServerController.start(cliMessageHandler, remotePreferences.getLanguageServerPort());
}
+
+ NativeDesktop.get().registerJabRefProtocolHandler(uri -> {
+ LOGGER.debug("Received URI via protocol handler: {}", uri);
+ Platform.runLater(() -> mainFrame.handleUiCommands(List.of(new UiCommand.Focus())));
+ });
}
private void setupHttpServerEnabledListener() {
diff --git a/jabgui/src/main/java/org/jabref/gui/desktop/os/NativeDesktop.java b/jabgui/src/main/java/org/jabref/gui/desktop/os/NativeDesktop.java
index a3f86c090f0..dcfbe92401f 100644
--- a/jabgui/src/main/java/org/jabref/gui/desktop/os/NativeDesktop.java
+++ b/jabgui/src/main/java/org/jabref/gui/desktop/os/NativeDesktop.java
@@ -9,6 +9,7 @@
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
+import java.util.function.Consumer;
import java.util.regex.Pattern;
import org.jabref.architecture.AllowedToUseAwt;
@@ -360,4 +361,13 @@ public void moveToTrash(Path path) {
public boolean moveToTrashSupported() {
return Desktop.getDesktop().isSupported(Desktop.Action.MOVE_TO_TRASH);
}
+
+ /// Registers a handler for the {@code jabref://} URL scheme so that the app is focused when a link is opened.
+ /// On macOS, URI invocations are delivered as Apple Events; this method registers the handler there.
+ /// On Windows and Linux the URL arrives as a CLI argument and is handled by [ArgumentProcessor], so this is a no-op.
+ ///
+ /// @param whenJabRefUriOpened callback invoked when a jabref:// URI is received (e.g. focus the main window; run on FX thread if needed)
+ public void registerJabRefProtocolHandler(Consumer whenJabRefUriOpened) {
+ // Default: no-op. Overridden in OSX.
+ }
}
diff --git a/jabgui/src/main/java/org/jabref/gui/desktop/os/OSX.java b/jabgui/src/main/java/org/jabref/gui/desktop/os/OSX.java
index 2d484620e22..f19474f0108 100644
--- a/jabgui/src/main/java/org/jabref/gui/desktop/os/OSX.java
+++ b/jabgui/src/main/java/org/jabref/gui/desktop/os/OSX.java
@@ -1,8 +1,11 @@
package org.jabref.gui.desktop.os;
+import java.awt.Desktop;
import java.io.IOException;
+import java.net.URI;
import java.nio.file.Path;
import java.util.Optional;
+import java.util.function.Consumer;
import org.jabref.architecture.AllowedToUseAwt;
import org.jabref.gui.DialogService;
@@ -10,12 +13,14 @@
import org.jabref.gui.externalfiletype.ExternalFileTypes;
import org.jabref.gui.frame.ExternalApplicationsPreferences;
+import org.slf4j.LoggerFactory;
+
/// This class contains macOS (OSX) specific implementations for file directories and file/application open handling methods.
///
/// We cannot use a static logger instance here in this class as the Logger first needs to be configured in the {@link JabKit#initLogging}.
/// The configuration of tinylog will become immutable as soon as the first log entry is issued.
/// https://tinylog.org/v2/configuration/
-@AllowedToUseAwt("Requires AWT to open a file")
+@AllowedToUseAwt("Requires AWT to open a file and to register the jabref:// protocol handler")
public class OSX extends NativeDesktop {
@Override
@@ -52,4 +57,22 @@ public void openConsole(String absolutePath, DialogService dialogService) throws
public Path getApplicationDirectory() {
return Path.of("/Applications");
}
+
+ @Override
+ public void registerJabRefProtocolHandler(Consumer whenJabRefUriOpened) {
+ if (!Desktop.isDesktopSupported()) {
+ return;
+ }
+ Desktop desktop = Desktop.getDesktop();
+ if (!desktop.isSupported(Desktop.Action.APP_OPEN_URI)) {
+ return;
+ }
+ desktop.setOpenURIHandler(event -> {
+ URI uri = event.getURI();
+ if ("jabref".equals(uri.getScheme())) {
+ whenJabRefUriOpened.accept(uri);
+ }
+ });
+ LoggerFactory.getLogger(OSX.class).debug("Protocol handler for jabref:// registered");
+ }
}
diff --git a/jabgui/src/test/java/org/jabref/cli/ArgumentProcessorTest.java b/jabgui/src/test/java/org/jabref/cli/ArgumentProcessorTest.java
new file mode 100644
index 00000000000..220dac2e5cd
--- /dev/null
+++ b/jabgui/src/test/java/org/jabref/cli/ArgumentProcessorTest.java
@@ -0,0 +1,73 @@
+package org.jabref.cli;
+
+import java.util.List;
+
+import org.jabref.gui.preferences.GuiPreferences;
+import org.jabref.logic.UiCommand;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+
+class ArgumentProcessorTest {
+
+ private final GuiPreferences preferences = mock(GuiPreferences.class);
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "jabref://",
+ "jabref://open",
+ "jabref:",
+ "jabref://some/path",
+ "JABREF://open",
+ "JabRef://open"})
+ void protocolHandlerUrlProducesFocusCommand(String url) {
+ ArgumentProcessor processor = new ArgumentProcessor(
+ new String[] {url},
+ ArgumentProcessor.Mode.REMOTE_START,
+ preferences);
+
+ List commands = processor.processArguments();
+
+ assertEquals(List.of(new UiCommand.Focus()), commands);
+ }
+
+ @Test
+ void normalArgumentsAreNotAffectedByProtocolFilter() {
+ ArgumentProcessor processor = new ArgumentProcessor(
+ new String[] {"--blank"},
+ ArgumentProcessor.Mode.REMOTE_START,
+ preferences);
+
+ List commands = processor.processArguments();
+
+ assertEquals(List.of(new UiCommand.BlankWorkspace()), commands);
+ }
+
+ @Test
+ void protocolHandlerUrlCombinedWithNormalArguments() {
+ ArgumentProcessor processor = new ArgumentProcessor(
+ new String[] {"jabref://", "--blank"},
+ ArgumentProcessor.Mode.REMOTE_START,
+ preferences);
+
+ List commands = processor.processArguments();
+
+ assertEquals(List.of(new UiCommand.BlankWorkspace()), commands);
+ }
+
+ @Test
+ void emptyArgumentsProduceNoFocusCommand() {
+ ArgumentProcessor processor = new ArgumentProcessor(
+ new String[] {},
+ ArgumentProcessor.Mode.REMOTE_START,
+ preferences);
+
+ List commands = processor.processArguments();
+
+ assertEquals(List.of(), commands);
+ }
+}
diff --git a/snap/gui/jabref.desktop b/snap/gui/jabref.desktop
index 3444b73fc8a..aa322a44add 100644
--- a/snap/gui/jabref.desktop
+++ b/snap/gui/jabref.desktop
@@ -9,4 +9,4 @@ Exec=jabref %U
Keywords=bibtex;biblatex;latex;bibliography
Categories=Office;
StartupWMClass=org-jabref-JabRefMain
-MimeType=text/x-bibtex;
+MimeType=text/x-bibtex;x-scheme-handler/jabref;