Open-source project
sk2andy/candy-browser avatar
sk2andy/candy-browser

Candy Browser puts Firefox extensions on Android and makes the sync server your job

Gesture-first Android browser with Material 3 Expressive design and local privacy tools

664 stars23 forksKotlinNOASSERTION

At a glance

What is it?
A gesture-first Kotlin browser that defaults to GeckoView so Mozilla-signed extensions run on Android, with a second engine behind the same UI and an end-to-end encrypted sync you have to host yourself.
Who is it for?
Candy Browser is worth a look if you want Firefox extensions on Android and can accept that the second engine is a restart away from your logins, that Firefox extensions exist only in the GeckoView process, and that sync means running a Docker compose stack before you can join your first device.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository received new commits within the last day.
What is it written in?
Mainly Kotlin, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on November 14, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • sk2andy/candy-browser664 ★ · +232
Star history
Repository2026-09-212026-11-14
sk2andy/candy-browser432664

232 Stars in the last 54 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

Switching engines restarts the app and strands your logins

The engine picker is not a per tab setting. Android defaults to GeckoView, and moving to Android System WebView happens for the whole app from Browser settings. Candy handles the handover by checkpointing tab URLs and restarting into a fresh process. Tabs, bookmarks, history, profiles and settings come across. Cookies, logins, native back-forward lists and other engine owned session state do not, because those belong to the engine rather than to the application. So the switch costs you a process restart and a signed out state, and the back button history goes with it. The trade is stated plainly: Firefox extensions remain available only in GeckoView, while System WebView keeps Toppings and Candy protection but loses the extension layer. There is no third engine in the picker and no per site override. The reason there are two at all is that Android System WebView is the system managed runtime whose version follows the operating system, while GeckoView is the engine that can host Mozilla signed Firefox extensions such as uBlock Origin.

Firefox extensions exist only inside the GeckoView process

A fresh Gecko profile arrives with two extensions already in place, uBlock Origin and I still don't care about cookies, and further compatible ones can be added from Candy's browser settings. What matters more than the count is where the whole extension surface lives. Actions, popups, the options page, permissions, updates and the enable and disable controls all stay inside Candy's own browser chrome instead of bouncing out to a separate app or a system settings screen. The project points readers at docs/browsing/platform-engines.md for the supported GeckoView integration and the current compatibility boundaries, and restates none of them on the main page. That boundary is worth pausing on, because a GeckoView extension is a Firefox extension, so the practical ceiling is whatever the embedded engine accepts rather than whatever an Android WebView would take. On the System WebView side the answer is no, and the documented substitute is Toppings, Candy's own userScripts based layer, which is also the part that survives an engine switch.

The sync extension is not in any store yet

Three of the four sync roadmap entries are still unchecked, and one of them decides how you get started. Publishing the extension to the Chrome Web Store and Mozilla Add-ons is open, so there is nothing to install. The setup path is to build and load the WebExtension from sync/extension/ yourself, open the browser managed Options Page it registers, and enter the endpoint plus an E2EE passphrase. On the phone the matching step is Settings, then Synchronization, then join with the same endpoint, credentials and passphrase. The one ticked box is the prebuilt server image on Docker Hub, so the asymmetry is deliberate: the server half is a pull, the client half is a local build. The consequence is that desktop and Android setup for one workspace run through two different configuration surfaces, and any mismatch in endpoint or passphrase surfaces as a failed join rather than a partial sync. Add multi-user support is also open, so a workspace carries a single identity model, and the fourth item, a hosted option for people who do not want to self host, means nobody else is going to run the server half on your behalf.

Self hosting is the only route to sync

The server half is a compose file and four values in an environment file. The published image is sk2andy/candy-sync:latest and it covers Linux AMD64 and ARM64, so the architecture question has an answer. Contributors without that image can build locally instead. What you must supply in .env is a username, a password, a public URL and a TLS host, and one value you must never supply is the E2EE passphrase, which the setup comment rules out of server configuration outright. That instruction is the whole security boundary in miniature: the server is reachable and authenticated, but the thing that decrypts your tabs does not live in its settings. Local TLS counts as part of this rather than an extra, since the guide the project points to, docs/sync/README.md, lists it alongside server deployment, Chromium and Firefox loading, Android setup, backups, protocol details and the full security model. None of that is on the main page, so the compose sequence is as far as it goes:

sh
cd sync/server
cp .env.example .env
# Set a unique username, password, public URL, and TLS host in .env.
# Never put the E2EE passphrase in server configuration.
docker compose pull candy-sync
docker compose up -d

Contributors can still build the server locally with docker compose up --build -d.

The passphrase is the only root, because the recovery key is local

Five primitives and one consequence. The first client to join creates a random 256 bit workspace key. The passphrase is immutable, and it derives a local recovery key through Argon2id. HKDF-SHA-256 then derives purpose specific and device specific keys from that, and AES-256-GCM both encrypts and authenticates the tab data. Every device separately generates its own P-256 private key, locally. None of the passphrase, the workspace key, the private keys, or the URLs, titles, device names and icons ever reach the server in plaintext. What the server holds is ciphertext plus the routing metadata it needs in order to move changes between clients. The design is a careful one, and the weight of it falls on the operator. Because the recovery key is derived locally from a passphrase the server never sees, the server holds nothing that could restore a workspace, and no server side reset path appears in the description. Every device that joins needs the same passphrase, and a lost passphrase is not a support ticket, it is an unreadable workspace.

WebSocket notifications are the accelerator, REST is the record

The sync model is closer to a message queue than to a state mirror. Changes are encrypted on the client and written to a durable outbox first. REST commits are what actually store them, and they are described as ordered and retry safe. Authenticated WebSocket notifications are a delivery fast path for changes that have already been committed, not a second copy of the data. When a phone is suspended or the network drops, the client does not resume over the socket; it recovers the missing changes through REST. So a bad connection costs latency rather than data, and the server's durable state is the ordered REST log. What travels through it is deliberately small. Candy Sync turns every connected Android, Chromium or Firefox device into a writable profile, so a phone can open a desktop profile, read its live tab list, open new tabs, navigate, pin, reorder or close them. Those small tab changes flow back out to every connected client instead of the complete browser state being re-uploaded, which is the difference between a phone acting as a remote control for a desktop session and a phone running a second full browser of its own.

iOS is called a target while iosApp and fastlane are already in the tree

The project description calls iOS a target, an iOS WKWebView and Liquid Glass target, and the architecture section describes the arrangement in the present tense: WKWebView behind the same shared Kotlin contracts and Compose browser chrome, with native Liquid Glass supplied through an iOS effect adapter. The tree is further along than the word target implies. It carries iosApp/ and fastlane/ beside the Android side, plus keystore.properties.example, distribution/, release-notes/, scripts/, site/ and .agents/ with an AGENTS.md. A top level listing that mixes a signing example, a release pipeline, a documentation site and an agent instructions file is a release engineering surface, and none of it is described on the main page. That same architecture diagram names the three injection hosts the single shared Topping model feeds, a Gecko userScripts host, a System WebView userScripts host and a WKUserScript host, which is how one feature set can be claimed for three engines. The paragraph then stops mid word, at Implementation boundaries, invariants, and th, and the invariants it was about to name live in docs/browsing/platform-engines.md instead.

Editorial conclusion

Candy Browser is worth a look if you want Firefox extensions on Android and can accept that the second engine is a restart away from your logins, that Firefox extensions exist only in the GeckoView process, and that sync means running a Docker compose stack before you can join your first device. Before you commit, check three things: which engine your daily extensions need, whether your desktop browser can load an unpacked WebExtension from sync/extension/, and whether you can keep a passphrase and its recovery path safe, because the server cannot help you there. The version is still below 1.0, and the store listing for the sync extension does not exist yet.

Frequently asked questions

What is Candy Browser and which rendering engines does it use?

It is a gesture-first Android browser written in Kotlin and released under MPL-2.0. It defaults to GeckoView, which lets it run Mozilla signed Firefox extensions, and it can switch the whole app to Android System WebView from Browser settings, keeping Toppings and Candy protection. iOS is described as a target using WKWebView with Liquid Glass supplied through an iOS effect adapter.

Can I run Firefox extensions such as uBlock Origin in Candy Browser?

Yes, in the GeckoView engine, which is the Android default. A fresh Gecko profile comes with uBlock Origin and I still don't care about cookies, further compatible extensions can be installed from browser settings, and actions, popups, options, permissions, updates and enable and disable controls stay inside Candy's own browser chrome. Firefox extensions are not available in the System WebView engine.

How do I set up Candy Sync, and do I have to run my own server?

Yes, you self host it. The server lives in sync/server, you copy .env.example to .env and set a username, password, public URL and TLS host, then run docker compose pull candy-sync and docker compose up -d using the public sk2andy/candy-sync:latest image for Linux AMD64 and ARM64. The E2EE passphrase must never go into server configuration. The roadmap still lists a hosted option for people who do not want to self host as unchecked.

What happens to my cookies and logins when I switch browser engines?

They stay behind. Switching restarts the app into a fresh process after checkpointing tab URLs, and tabs, bookmarks, history, profiles and settings carry over, while cookies, logins, native back-forward lists and other engine owned session state remain separate. That is the trade for gaining Android System WebView, and it comes with the loss of Firefox extensions.

Is Candy Browser available for iOS yet?

The project description calls iOS a target, and the architecture section describes WKWebView behind the shared Kotlin contracts with native Liquid Glass from an iOS effect adapter. The repository tree already contains iosApp/ and fastlane/ directories, but the main page gives no iOS download or build step, and the listed releases v0.44.0, v0.43.0 and v0.41.3 are Android builds with no platform stated.

Official sources

  1. Issues
  2. License: MPL-2.0
  3. README
  4. Releases
  5. sk2andy/candy-browser on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/sk2andy-candy-browser.svg)](https://hysenlabs.com/projects/sk2andy-candy-browser)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/sk2andy-candy-browser.svg)](https://hysenlabs.com/projects/sk2andy-candy-browser)

Was this useful?