GhaziAlibi/helixsync
GhaziAlibi/helixsync has 26 GitHub stars and 1 forks. GitHot measured 0 stars in 24 hours and -4 in 7 days. Last checked 2026-11-15 03:24 UTC. Last push 2026-11-06.
Measured star growth
- in 24h
- 0
- in 7 days
- -4
- in 15 days
- Collecting history
- in 1 month
- Collecting history
Net star changes over overlapping periods, not daily totals. Missing measurements are left blank. Stars measure attention, not software quality.
About
Keep your bookmarks, browsing history, and tabs in sync across your devices. Your data stays yours.
Self-hostedREADME.md
OriginalHelixSync
HelixSync is a self-hosted, end-to-end encrypted sync server for your browser. It keeps bookmarks, history, and (optionally) open tabs, windows and tab groups in sync across your own devices, through a server you run yourself, with no Google, Microsoft, or other third-party account involved.
It works with Helium and any other Chromium-based browser (Chrome, Brave, Edge, Vivaldi, ...) through a Manifest V3 extension (Chromium 116 or newer).
- You own the data. Everything lives in a PostgreSQL database on your own server.
- End-to-end encrypted. The server only stores ciphertext; it cannot read your bookmarks or history. Your password never leaves your devices.
- Multi-device. Connect as many browsers as you like, and review or revoke each one from the web dashboard.
- Real conflict resolution instead of last-write-wins, built on an
operation log. See
docs/protocol.md. - Simple to run. One
docker compose upstarts the API server, the database and the web dashboard. No third-party service is involved.
Passwords, cookies and site sessions are deliberately not synced; use a password manager such as Vaultwarden for passwords.
What it syncs
| Data | Status |
|---|---|
| Bookmarks | Full sync, including folders and moves |
| History | Synced into the browser's own history; see Limitations for what Chrome's API can't set |
| Tabs / windows | Optional, restored automatically or after asking, as you choose |
| Tab groups | Synced, including title, color and collapsed state |
| Restore points | Encrypted snapshots of your bookmarks you can roll back to, in full or one folder |
| Passwords | Out of scope |
How it fits together
Browser #1 (extension) ─┐ ┌─ Web dashboard (browser)
│ HTTPS / WebSocket │
Browser #2 (extension) ─┴──► Reverse proxy ◄──────┘
(TLS, Caddy / nginx / Traefik ...)
│
▼
web (nginx: dashboard + /api proxy)
│
▼
server (Rust API + WebSocket)
│
▼
PostgreSQL
| Directory | What it is |
|---|---|
server/ |
Rust (axum, SQLx) sync API and WebSocket server, backed by PostgreSQL |
web/ |
React + TypeScript dashboard: accounts, devices, sync settings, security |
extension/ |
TypeScript Manifest V3 browser extension, the sync client |
docs/ |
Architecture, protocol, encryption, security and deployment documents |
Getting started
Installing HelixSync takes four steps: run the server, create your account, install the extension, and connect your browser.
1. Run the server
You need Docker with the Compose plugin for options A to C (option E needs no Docker). Pick the deployment that suits you:
| Option | Best for | Needs |
|---|---|---|
| A. Docker Compose, prebuilt images | Most self-hosters | Docker |
| B. Docker Compose, build from source | Trying local changes, auditing what you run | Docker, a repo checkout |
| C. Portainer or another stack UI | Managing containers through a UI, no checkout on the host | Docker, Portainer |
| D. Behind a reverse proxy with HTTPS | Any use from another machine | A domain name, a TLS proxy |
| E. Without Docker | Bare-metal or VM installs | Rust, Node.js, PostgreSQL 16 |
F. Local trial on localhost |
Looking around before committing | Docker |
HTTPS is required for anything but localhost. The dashboard's session
cookies are marked Secure, so a browser won't keep you signed in over plain
HTTP, and the extension refuses a Server URL that isn't https:// (only
localhost, 127.0.0.1 and [::1] may use http://). Options A to C start
an HTTP server on port 5173; put a TLS-terminating proxy in front of it
(option D) before using it from another machine.
Configure the secrets (options A to D)
Every Compose-based option reads its settings from a .env file. Get the
repository (for option C you only need the file contents, so you can skip the
clone), create .env from the example, and fill in the two secrets:
git clone https://github.com/GhaziAlibi/helixsync.git
cd helixsync
cp .env.example .env
sed -i "s|^POSTGRES_PASSWORD=.*|POSTGRES_PASSWORD=$(openssl rand -hex 32)|" .env
sed -i "s|^JWT_SIGNING_KEY=.*|JWT_SIGNING_KEY=$(openssl rand -base64 48)|" .env
(That is GNU sed; on macOS use sed -i ''.) Then:
POSTGRES_PASSWORDprotects the database. Use a hex value as above: the password is embedded in a connection URL, where characters such as/or@would break it. The server refuses to start while it still contains the placeholder text.JWT_SIGNING_KEYsigns device access tokens. The server refuses to start while it still contains the placeholder text (change-meorreplace-me). Keep it stable: changing it invalidates every outstanding access token (refresh tokens are unaffected).- Set
CORS_ALLOWED_ORIGINSto your public dashboard origin, for examplehttps://sync.example.com. Only requests from a different origin are checked against it; with the default single-origin setup it is not used.
The other variables are optional; see Configuration.
A. Docker Compose with prebuilt images (recommended)
Pulls the images published for every release to GitHub Container Registry, so nothing is compiled on your machine.
# in the helixsync directory, with .env created as above:
# set HELIXSYNC_VERSION in .env to a released version, for example
# HELIXSYNC_VERSION=1.0.0
docker compose -f docker-compose.prod.yml up -d
HELIXSYNC_VERSION is required: the stack refuses to start without it rather
than following a moving latest tag. See
Releases for the available
versions. Database migrations run automatically when the server starts.
B. Docker Compose, built from source
Builds the server and web images locally from the repository:
# in the helixsync directory, with .env created as above
docker compose up -d --build
The first build takes a few minutes (it compiles the Rust server and builds
the dashboard). The dashboard is then at http://localhost:5173.
C. Portainer or another stack UI
docker-compose.prod.yml is self-contained and
pulls prebuilt images, so you can paste it into Portainer's Stacks → Add
stack → Web editor with no files on the host. Add the variables from
.env.example (at least POSTGRES_PASSWORD, JWT_SIGNING_KEY,
CORS_ALLOWED_ORIGINS and HELIXSYNC_VERSION) in the stack's environment
section, and deploy. Any other tool that accepts a Compose file works the same
way.
D. Reverse proxy with HTTPS
Options A to C leave web listening on WEB_PORT (5173). To use HelixSync
from other machines, put a TLS-terminating proxy in front of it. The proxy
must forward WebSocket upgrades (the extension uses one) and pass the client's
scheme (X-Forwarded-Proto) and address (X-Forwarded-For) on.
Caddy does all of that by default. If it runs in
the same Docker network as web:
sync.example.com {
reverse_proxy web:80
}
For nginx:
location / {
proxy_pass http://web:80;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# nginx's default request limit (1 MB) is far below the server's upload
# limit, so raise it, or large history uploads fail with 413.
client_max_body_size 150m;
proxy_read_timeout 3600s;
}
Traefik and other proxies need the same three things (and, like nginx, must
allow large request bodies). Details, including how
real client IPs reach the rate limiter and when to narrow
WEB_TRUSTED_PROXY_CIDRS, are in docs/deployment.md.
E. Without Docker
Run the server binary and PostgreSQL directly, and serve the dashboard's static files from a web server of your choice. You need a stable Rust toolchain with a C compiler, Node.js 22, and PostgreSQL 16.
git clone https://github.com/GhaziAlibi/helixsync.git
cd helixsync
# 1. Database (run as the PostgreSQL superuser; use a plain alphanumeric password)
sudo -u postgres createuser helixsync
sudo -u postgres createdb -O helixsync helixsync
sudo -u postgres psql -c "ALTER ROLE helixsync PASSWORD 'choose-a-password'"
# 2. Server. Builds offline from the committed query cache: no database is needed to compile.
cd server
SQLX_OFFLINE=true cargo build --release --locked
# 3. Dashboard: builds static files into web/dist
cd ../web
npm ci
npm run build
Configure the server with a .env file in the directory you run it from (it
is read at startup and is git-ignored):
cd ../server
cp .env.example .env
chmod 600 .env
Edit .env: set DATABASE_URL to
postgres://helixsync:[email protected]:5432/helixsync, set
JWT_SIGNING_KEY to the output of openssl rand -base64 48, and add
BEHIND_PROXY=true and CORS_ALLOWED_ORIGINS=https://sync.example.com. Keep
BIND_ADDR=127.0.0.1:8080, so the proxy below is the only way in. Then start
it:
./target/release/helixsync-server
The server applies its database migrations itself on startup. To keep it
running, use a service manager; a systemd unit is in
docs/deployment.md.
Finally, serve web/dist and send /api/ to the server from one origin, with
TLS. For example with Caddy, after copying web/dist to /var/www/helixsync:
sync.example.com {
handle /api/* {
reverse_proxy 127.0.0.1:8080 {
# the server trusts this header for rate limiting (BEHIND_PROXY=true)
header_up X-Real-IP {remote_host}
}
}
handle {
root * /var/www/helixsync
try_files {path} /index.html
file_server
}
}
Set BEHIND_PROXY=true only when a proxy you control is the sole way to reach
the server, as here. This setup doesn't get the security headers that the
web image's nginx adds; copy them from
web/security-headers.conf into your proxy.
F. Local trial on localhost
To look around without a domain or certificate, use option B and open
http://localhost:5173. In the extension, use http://localhost:5173 as the
Server URL; loopback addresses are the one case where plain HTTP is allowed.
Compose publishes WEB_PORT on all network interfaces by default. On a shared
network, change the web service's port mapping to
"127.0.0.1:${WEB_PORT:-5173}:80" so only your machine can reach it.
2. Create your account
Open the dashboard in your browser (https://sync.example.com, or
http://localhost:5173 for a trial) and choose Register. Enter an email
and a password. The account key that encrypts your data is created in your
browser and locked with this password, so neither the server operator nor the
developer can reset it or read your data without it.
The dashboard then shows your recovery code, once. Save it somewhere safe and offline (print it, or keep it in a password manager): if you forget your password, it's the only way to set a new one without losing your data.
Registration is open by default. On any instance reachable from the internet, close it once your accounts exist (see Lock it down).
3. Install the extension
Pick one:
Chrome Web Store: HelixSync.
Prebuilt zip: download
helixsync-extension-<version>.zipfrom the Releases page, unzip it, openchrome://extensions(or your browser's equivalent), enable Developer mode, choose Load unpacked, and select the unzipped folder.From source:
cd extension npm ci npm run buildThen load
extension/dist/as an unpacked extension, as above.
4. Connect your browser, and lock the server down
- Click the HelixSync toolbar icon. In Connect this browser, enter the
Server URL (the same origin as your dashboard, for example
https://sync.example.com), your Email, your Password, and a Device name for this browser. - Click Connect. The browser asks for permission to talk to that one server address; allow it. The extension requests access to that origin only, never to all sites.
- Repeat on every browser you want to sync. Each one signs in with the same email and password and ends up with the same account key. Instead of the password, you can click Log in with device instead and approve the request from the popup of a browser that's already connected, after checking that both show the same phrase.
- Close registration. Once your accounts exist, set
ALLOW_REGISTRATION=falsein.envand restart the server (docker compose up -d, or restart the process for option E). Existing accounts, logins and devices are unaffected.
Initial sync starts right away. Large histories are uploaded in chunks in the background, so the first full sync of a big profile can take a while.
Using HelixSync
Extension popup. The popup is the whole extension UI. It shows the sync status and when it last synced. The gear icon opens the settings once connected: which data to sync (bookmarks, history, tabs, tab groups), the tab restore policy (disabled, ask before restoring, or restore automatically), and history retention (7 days to unlimited). Disconnect revokes the device on the server and removes its local data. Under Account, Change on the web opens the dashboard's password form. When another browser or the dashboard asks to log in with this device, the toolbar icon shows ! and the popup lists the request with its phrase, to approve or deny.
Restore points. The clock icon in the popup lists the saved copies of your
bookmarks. Your browsers make one daily, and before a restore, a first import,
a full resync, or a large deletion; Create restore point makes one on
demand. Restore… rolls back everything or one folder: Merge brings back
what is missing and undoes edits without deleting anything, Exact also
removes bookmarks the point doesn't have. A preview shows what would change,
and a copy of your current bookmarks is saved first, so a restore can itself be
undone. Only pinned points are kept for good: unpinned ones are removed
oldest first when space runs out, and after 90 days. See
docs/protocol.md §16.
Web dashboard.
| Page | What you can do |
|---|---|
| Dashboard | Connection and sync health, last sync, device count, how many bookmarks, history items and tabs are synced, storage used against your limit, and recent devices |
| Devices | List, rename and revoke connected browsers |
| Synchronization | The same sync settings as the popup, kept in one place |
| Restore points | See, pin and delete your bookmark restore points, switch automatic ones off, and see the space they use. Restoring is done in the extension |
| Security | Check encryption status, review and revoke web sessions, change your password, get a new recovery code, delete your account |
Changing your password re-wraps your account key under the new password. Nothing already synced becomes unreadable; each browser simply signs in again with the new password.
Forgot your password? On the sign-in page, enter your email, your
recovery code and a new password. Your data stays readable, every web session
is signed out, and you get a new recovery code (each one works once). To just
get into the dashboard, Log in with a device lets a connected browser
approve you. If you lost the recovery code as well, the server's operator can
delete the account with helixsync-server delete-account <email> so you can
register again; your browsers upload their bookmarks and history again when
you reconnect them. See docs/encryption.md §9.
Deleting your account (Security → Danger zone) permanently removes the
account and everything stored under it, and connected browsers stop syncing.
It asks for your password and for you to type DELETE.
Configuration
Compose reads these from .env (see .env.example, which
documents each one; every setting the server reads is listed in
docs/deployment.md). The ones
you are most likely to touch:
| Variable | Default | Purpose |
|---|---|---|
POSTGRES_PASSWORD |
none, required | Database password |
JWT_SIGNING_KEY |
none, required | Signs device access tokens; at least 32 characters, never a placeholder |
HELIXSYNC_VERSION |
none, required for docker-compose.prod.yml |
Released image version to run |
CORS_ALLOWED_ORIGINS |
empty | Origins allowed to call the API from a different origin |
ALLOW_REGISTRATION |
true |
Set to false to close sign-ups once your accounts exist |
REQUIRE_ENCRYPTION |
true |
Reject unencrypted sync data. Leave it on for real data |
MAX_ACCOUNT_STORAGE_BYTES |
5 GiB | Cap on stored sync data per account |
WEB_PORT |
5173 |
Host port the dashboard listens on |
WEB_TRUSTED_PROXY_CIDRS |
private ranges | Proxies whose X-Forwarded-For is trusted; narrow it if web is published directly |
LOG_FORMAT |
text |
json writes one JSON object per log line, for a log collector |
Running the server outside Docker uses the same names plus DATABASE_URL,
BIND_ADDR (default 127.0.0.1:8080) and BEHIND_PROXY; see
server/.env.example and
docs/deployment.md. The server checks its settings at
startup and refuses to start, naming the variable, when one is invalid.
Before you expose it to the internet
- HTTPS through a reverse proxy in front of
web. Nothing here works properly over plain HTTP. - Strong, unique
POSTGRES_PASSWORDandJWT_SIGNING_KEY. -
REQUIRE_ENCRYPTIONleft at its default (true). -
ALLOW_REGISTRATION=falseonce your accounts exist, or anyone who finds the URL can sign up and store up toMAX_ACCOUNT_STORAGE_BYTES(5 GiB by default) each on your server. - Backups of the database, with a tested restore (see below). Backups don't help with a forgotten password; only the user's recovery code does.
-
HELIXSYNC_VERSIONpinned to a released version when usingdocker-compose.prod.yml, and updated deliberately. - If
webis published directly rather than reached only through your proxy on the Docker network, narrowWEB_TRUSTED_PROXY_CIDRS, or bindWEB_PORTto loopback.
Updating and backups
Update by choosing a newer HELIXSYNC_VERSION in .env (prebuilt images) and
restarting:
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d
If you build from source, pull the repository and rebuild instead
(git pull && docker compose up -d --build).
Read the changelog first: a release can contain breaking changes, and its upgrade notes say what to do. Migrations run automatically.
Back up the database with pg_dump (adjust the user and database name if you
changed POSTGRES_USER or POSTGRES_DB):
docker compose exec postgres pg_dump -U helixsync helixsync > backup.sql
The dump holds each account's key only in wrapped form, so it is useless
without the account password; keep it as private as the database itself, since
it also contains account emails and device names. Restore and disaster
recovery steps are in docs/deployment.md.
Troubleshooting
| Symptom | Cause and fix |
|---|---|
server exits at startup mentioning JWT_SIGNING_KEY |
The key is missing, shorter than 32 characters, or still contains change-me / replace-me. Generate one with openssl rand -base64 48. |
server exits at startup mentioning DATABASE_URL and a placeholder |
POSTGRES_PASSWORD still has the .env.example value. Set it with openssl rand -hex 32. PostgreSQL keeps the password it was first started with, so if the stack already ran once, also run docker compose exec postgres psql -U helixsync -c "ALTER ROLE helixsync PASSWORD '<the new value>'". |
server exits at startup naming another variable |
Its value doesn't parse or is out of range; the message says what is expected. See docs/deployment.md. |
docker compose says HELIXSYNC_VERSION is missing a value |
docker-compose.prod.yml needs a released version in .env, for example HELIXSYNC_VERSION=1.0.0. |
The extension says the Server URL must use https:// |
Only localhost, 127.0.0.1 and [::1] may use plain HTTP. Put a TLS proxy in front of web (option D). |
| The dashboard signs you out immediately, or warns that the page is not using HTTPS | Session cookies are Secure; load the dashboard over HTTPS (or from localhost). |
Registering returns registration_disabled |
The server has ALLOW_REGISTRATION=false. Set it to true temporarily to add an account. |
| A user forgot their password | They reset it with their recovery code (Forgot your password? on the sign-in page). Without the code, run docker compose exec server /app/helixsync-server delete-account <email> once you've checked who's asking, and they register again. |
| The extension shows an "update the HelixSync extension" error | It met sync data written in a newer format. Update the extension; nothing is lost, and it retries automatically. |
Everyone gets 429 rate-limit errors after one client misbehaves |
The rate limiter sees all clients as the proxy's address. Make sure your proxy sets X-Forwarded-For, and see docs/deployment.md. |
For anything else, check docker compose logs server and the extension's
service-worker console, then open an
issue (without secrets or
synced data in it).
Limitations
- History from other devices arrives dated when it was synced, without a
title. HelixSync adds each synced URL to the browser's own history, so it
shows up in
chrome://historyand address-bar suggestions. Chrome's extension APIs can't write a visit with a historical timestamp or title (chrome.history.addUrlcan only add "visited now", with no title), so the entry is dated when it was synced and stays untitled until you open the page. Each distincthttp/httpsURL is added once, and a URL your browser already has is left alone. Every visit is read, including a whole first import, so a large profile takes a while: the work is paced to keep CPU use low. - Retention buckets are hourly, not exact. The dashboard's "History items synced" count is tracked in per-hour buckets, so a visit right at the edge of your retention window may count for up to an hour longer or shorter than the exact cutoff.
- A URL whose only visits are hidden redirect hops can't always be counted. Chrome's history APIs don't expose every intermediate redirect visit to extensions, so a small number of such visits may not be enumerable during import.
- Losing both your password and your recovery code loses your data. Your
data is encrypted with a random account key that only your password or your
recovery code can unlock; the server never sees either, so neither the
operator nor the developer can reset them or get your data back. The operator
can only delete the account so you can start over. Keep the recovery code
private: like the password, it unlocks everything. See
docs/encryption.md§9. - One server process per database. Rate limits, short-lived auth caches
and WebSocket notifications are kept in the server's memory, so running
several replicas behind a load balancer isn't supported. See
docs/deployment.md. - Use the web dashboard only on a server you trust. Its code is served by
that server, so a compromised server could capture the password you type
there. The extension is installed separately and isn't affected. See
docs/security.md. - Tab groups need a permission in each browser. Turning on Tab Groups in a
browser's popup asks for it; when the setting was turned on elsewhere, open
the popup's settings and press Save. Tab, tab group and restore behavior is
tested end to end in Chromium and Helium (
CONTRIBUTING.md); reports from other Chromium builds are welcome.
Documentation
| Document | Contents |
|---|---|
docs/deployment.md |
Operating a server: reverse proxy, real client IPs, configuration, backups, migrations, health checks, account removal |
docs/architecture.md |
Components, data flow and deployment topology |
docs/protocol.md |
The sync protocol and REST/WebSocket API, the source of truth for sync behavior |
docs/encryption.md |
Key hierarchy, key derivation, the encryption envelope and its limits |
docs/security.md |
Security model, threat model, hardening and known limitations |
docs/releasing.md |
How releases are built and published (maintainers) |
PRIVACY.md |
What the extension accesses and where data goes |
SECURITY.md |
How to report a vulnerability privately |
CHANGELOG.md |
Release notes and upgrade notes |
Contributing
Contributions are welcome. CONTRIBUTING.md covers the
development setup for each component, the checks CI runs, and how to propose a
change; please also read the Code of Conduct. Read the
protocol and encryption documents before sending a pull request that touches
sync behavior.
Found a security problem? Please report it privately, as described in
SECURITY.md, rather than in a public issue.
License
MIT. Each build carries the licenses of the third-party code it
includes, in THIRD_PARTY_LICENSES.txt: at /app/ in the server image, at the
root of the dashboard (https://sync.example.com/THIRD_PARTY_LICENSES.txt),
and in the extension zip.