Syncs Bear notes with external consumers. Two components: hub (API server on VPS) and salmon-run (Mac agent that reads Bear SQLite).
Bear catches salmon — Bear.app is the source of truth for notes, and Salmon is the data that flows through the system. The salmon run (upstream migration) represents data flowing from Bear to the hub. The hub serves data downstream to consumers like a salmon stream. The bidirectional flow of data — notes flowing upstream from Bear to consumers, and write operations flowing back downstream to Bear — mirrors the salmon lifecycle.
Bear — source of truth for all note content. Stores notes in a local SQLite database (Core Data schema). Salmon Run reads this database directly and applies writes via Bear's x-callback-url scheme.
Salmon Run (bin/salmon-run) — Mac agent that runs on the same machine as Bear. Runs in daemon mode (--daemon) with a continuous sync loop, managed by SalmonRun.app. Reads Bear's SQLite, detects changes since the last run, pushes them to the hub, and pulls pending write operations from the hub to apply back to Bear via bear-xcall.
SalmonRun.app — native macOS menu bar application that wraps the salmon-run binary. Provides a GUI for monitoring sync status, viewing logs, triggering manual syncs, and configuring settings. The salmon-run process runs as a managed child process in daemon mode.
Hub (bin/salmon-hub) — API server that runs on a VPS. Acts as a read replica of Bear's notes and exposes a REST API for external consumers. Holds a write queue for consumer-initiated changes that need to propagate back to Bear.
Consumers — external applications that read and write notes via the hub API. Each consumer is identified by name and authenticated with its own token. Multiple consumers can be configured simultaneously. Consumers communicate only with the hub; never touch Bear or Salmon Run directly.
graph TB
subgraph mac["Mac (user's machine)"]
Bear["Bear.app\n(SQLite source of truth)"]
SalmonRunApp["SalmonRun.app\n(menu bar UI)"]
SalmonRun["salmon-run --daemon\n(sync loop)"]
bearxcall["bear-xcall CLI\n(x-callback-url executor)"]
end
subgraph vps["VPS"]
Caddy["Caddy\n(TLS reverse proxy)"]
Hub["salmon-hub\n(REST API + SQLite)"]
end
Consumer["Consumer\n(API client)"]
SalmonRunApp -- "manages process\nstdout + IPC socket" --> SalmonRun
SalmonRun -- "reads Bear SQLite\n(read-only)" --> Bear
SalmonRun -- "applies writes via\nbear:// URL scheme" --> bearxcall
bearxcall -- "x-callback-url" --> Bear
SalmonRun -- "POST /api/sync/push\n(bridge token)" --> Caddy
SalmonRun -- "GET /api/sync/queue\nPOST /api/sync/ack" --> Caddy
Caddy --> Hub
Consumer -- "GET/POST/PUT/DELETE /api/notes/, /api/tags/\n(consumer token)" --> Caddy
The sync_status field on each hub note guards against write conflicts between consumers and Bear.
stateDiagram-v2
synced: synced\n(normal state)
pending: pending_to_bear\n(consumer write queued)
conflict: conflict\n(Bear changed while write pending)
[*] --> synced: Bear push (initial/delta)
synced --> pending: consumer enqueues write\n(POST/PUT/DELETE)
pending --> synced: Salmon Run ACKs applied
pending --> conflict: Bear push arrives\nwith overlapping content change
conflict --> synced: Salmon Run creates [Conflict] note\nand ACKs conflict_resolved=true
While a note is pending_to_bear, Bear delta pushes do not overwrite title/body on the hub. Conflict detection is field-level: the hub snapshots Bear's title/body when transitioning to pending_to_bear, and a conflict is raised only if Bear changed a content field (title or body) that the consumer also changed. Metadata-only changes (e.g., opening the note in Bear) do not trigger a conflict. On conflict, Salmon Run creates a [Conflict] Title note in Bear instead of applying the queued write.
Consumers can enqueue write operations via the hub API. Salmon Run picks them up and applies them to Bear via x-callback-url.
| Action | Consumer API | Description |
|---|---|---|
create |
POST /api/notes |
Create a new note |
update |
PUT /api/notes/{id} |
Update note title/body |
add_tag |
POST /api/notes/{id}/tags |
Add a tag to a note |
trash |
DELETE /api/notes/{id} |
Move note to trash |
add_file |
POST /api/notes/{id}/attachments |
Attach a file to a note (multipart, 5 MB limit) |
archive |
POST /api/notes/{id}/archive |
Archive a note |
rename_tag |
PUT /api/tags/{id} |
Rename a tag |
delete_tag |
DELETE /api/tags/{id} |
Delete a tag |
All mutating consumer endpoints require an Idempotency-Key header. Encrypted notes are read-only (403).
- Go 1.26+
- Xcode Command Line Tools (for building bear-xcall and SalmonRun.app on macOS; provides
swiftc) - Bear.app (for salmon-run)
- bear-xcall CLI (built via
make build-xcall, for salmon-run write operations; source intools/bear-xcall/)
make build
Binaries are placed in bin/salmon-hub, bin/salmon-run, bin/bear-xcall.app, and bin/SalmonRun.app (macOS only).
| Variable | Required | Default | Description |
|---|---|---|---|
SALMON_HUB_HOST |
No | 127.0.0.1 |
Listen host (0.0.0.0 for Docker) |
SALMON_HUB_PORT |
No | 7433 |
Listen port |
SALMON_HUB_DB_PATH |
Yes | — | Path to SQLite database file |
SALMON_HUB_CONSUMER_TOKENS |
Yes | — | Consumer tokens in name:token format, comma-separated (e.g. openclaw:secret1,myapp:secret2) |
SALMON_HUB_BRIDGE_TOKEN |
Yes | — | Bearer token for Salmon Run sync access |
SALMON_HUB_ATTACHMENTS_DIR |
No | attachments |
Directory for attachment file storage |
export SALMON_HUB_DB_PATH=/opt/salmon/data/hub.db
export SALMON_HUB_CONSUMER_TOKENS="openclaw:secret1,myapp:secret2"
export SALMON_HUB_BRIDGE_TOKEN=<token>
./bin/salmon-hub
The hub listens on 127.0.0.1:PORT (localhost only). Use a reverse proxy (e.g. Caddy) for TLS termination.
sudo cp deploy/salmon-hub.service /etc/systemd/system/
sudo systemctl enable salmon-hub
sudo systemctl start salmon-hub
Create /opt/salmon/.env with the environment variables above.
- Create a
.envfile with your secrets:
SALMON_HUB_CONSUMER_TOKENS="openclaw:secret1,myapp:secret2"
SALMON_HUB_BRIDGE_TOKEN=<token>
DOMAIN=salmon.example.com
- Start the stack:
docker compose up -d
This starts the hub server and Caddy reverse proxy with automatic TLS. The hub is accessible only through Caddy (ports 80/443).
To check status:
docker compose ps
curl https://your-domain.com/healthz
To update to a new version:
docker compose pull
docker compose up -d
Data is persisted in Docker named volumes (hub-data for SQLite + attachments).
The hub container runs as non-root user hub (UID 1000). When using bind mounts, ensure the host directory is owned by UID 1000, otherwise SQLite will fail with unable to open database file: out of memory (14):
mkdir -p /volume1/docker/salmon_hub/attachments
chown -R 1000:1000 /volume1/docker/salmon_hub
This is not needed for Docker named volumes — they inherit permissions from the image automatically.
| Variable | Required | Default | Description |
|---|---|---|---|
SALMON_HUB_URL |
Yes | — | Hub API URL (e.g. https://salmon.example.com) |
SALMON_HUB_TOKEN |
Yes | — | Bearer token matching SALMON_HUB_BRIDGE_TOKEN |
SALMON_BEAR_TOKEN |
Yes | — | Token for Bear x-callback-url API (any string, e.g. openssl rand -base64 32; Bear will prompt to allow access on first use) |
SALMON_STATE_PATH |
No | ~/.salmon-state.json |
Path to salmon-run state file |
SALMON_BEAR_DB_DIR |
No | ~/Library/Group Containers/9K33E3U3T4.net.shinyfrog.bear/Application Data |
Path to Bear Application Data directory |
SALMON_SYNC_INTERVAL |
No | 300 |
Sync interval in seconds (daemon mode only) |
SALMON_IPC_SOCKET |
No | ~/.salmon.sock |
Unix socket path for IPC (daemon mode only) |
export SALMON_HUB_URL=https://salmon.example.com
export SALMON_HUB_TOKEN=<token>
export SALMON_BEAR_TOKEN=<token>
./bin/salmon-run
CLI flags:
--daemon— run continuously with periodic sync (default interval: 5 minutes)--version— print version and exit
The recommended way to run salmon-run is via the Menu Bar App, which manages it in daemon mode with a GUI.
SalmonRun.app is a native macOS menu bar application (macOS 14+) that manages salmon-run as a child process in daemon mode and provides a GUI for monitoring and configuration.
The app lives in the macOS menu bar with a sync icon that changes color based on status:
┌─────────────────────────┐
│ ● Synced │ ← green (idle), yellow (syncing), red (error)
│ Last sync: 2 min ago │
│ ─────────────────────── │
│ ▸ Sync Now │ ← triggers immediate sync
│ ─────────────────────── │
│ Notes: 1,234 │
│ Tags: 56 │
│ Queue: 0 pending │
│ ─────────────────────── │
│ ▸ View Logs... │ ← opens log viewer window
│ ▸ Settings... │ ← opens settings window
│ ─────────────────────── │
│ ▸ Quit Salmon Run │
└─────────────────────────┘
- Menu bar icon with color-coded sync status (green/yellow/red)
- One-click "Sync Now" to trigger immediate sync
- Live statistics: notes, tags, and write queue counts
- Log viewer window with search, level filtering, and auto-scroll
- Settings window with Hub URL, tokens (Keychain-secured), sync interval, and Launch at Login
- macOS notifications on sync errors (rate-limited, configurable)
- Auto-restart of salmon-run process on unexpected exit (up to 3 retries)
Settings are accessible from the menu bar popup via "Settings...":
| Tab | Setting | Storage | Description |
|---|---|---|---|
| Connection | Hub URL | UserDefaults | URL of your Salmon hub server |
| Connection | Hub Token | Keychain | Salmon Run authentication token (matches SALMON_HUB_BRIDGE_TOKEN) |
| Connection | Bear Token | Keychain | Token for Bear x-callback-url API |
| Sync | Sync interval | UserDefaults | How often to sync (1-30 minutes, default 5) |
| Sync | Sync on launch | Always on | Automatically syncs when the app starts |
| General | Launch at Login | SMAppService | Auto-start SalmonRun.app on login |
| General | Notifications | UserDefaults | Show macOS notifications on sync errors |
Tokens are stored securely in the macOS Keychain. All other settings use UserDefaults. The app generates environment variables for the salmon-run process from these settings.
Download the .dmg for your architecture from the Releases page:
SalmonRun-vX.Y.Z-arm64.dmg(Apple Silicon)SalmonRun-vX.Y.Z-amd64.dmg(Intel)
Open the .dmg and drag SalmonRun.app to /Applications. Launch from Applications and configure your Hub URL and tokens in Settings.
From source:
make build-app
cp -R bin/SalmonRun.app /Applications/
make build-app # Build SalmonRun.app (includes salmon-run + bear-xcall)
make test-app # Run Swift tests
A sample Caddyfile is provided in deploy/Caddyfile for systemd setup. For Docker Compose, deploy/Caddyfile.docker is used automatically.
The sample Caddyfile uses rate limiting, which requires the caddy-ratelimit plugin. Build Caddy with this plugin using xcaddy:
xcaddy build --with github.com/mholt/caddy-ratelimit
The hub serves interactive API documentation via Swagger UI at /api/docs/ (requires consumer auth).
To regenerate the OpenAPI spec after changing handler annotations:
make swagger
For a quick start guide with curl examples and integration details, see docs/consumer-api.md.
make test # run all tests
make test-race # run tests with race detector
make test-xcall # run bear-xcall manual tests (macOS + Bear)
make test-app # run SalmonRun Swift tests (macOS only)
make build-xcall # build bear-xcall .app bundle (macOS only)
make build-app # build SalmonRun.app menu bar app (macOS only)
make lint # run golangci-lint
make fmt # format code
make tidy # go mod tidy
make swagger # generate Swagger docs (swag init)
GitHub Actions runs automatically:
- CI (push/PR to main): lint, test, test with race detector
- Docker Publish (push tag
v*): builds multi-platform hub image (linux/amd64,linux/arm64) and pushes toghcr.io/romancha/salmon-hub - Release (push tag
v*): builds, signs, notarizes, and publishes SalmonRun.app as .dmg for macOS (arm64,amd64) as GitHub Release assets
Tag and push to trigger both Docker and release workflows:
git tag v0.1.0
git push origin v0.1.0
Pre-release tags (e.g., v0.1.0-rc.1) are automatically marked as pre-releases on GitHub.
The release workflow requires Apple code signing credentials. Set these in the repository settings under Settings > Secrets and variables > Actions:
| Secret | Description |
|---|---|
APPLE_CERTIFICATE |
Base64-encoded Developer ID Application .p12 certificate (base64 -i cert.p12 | pbcopy) |
APPLE_CERTIFICATE_PASSWORD |
Password for the .p12 certificate |
APPLE_TEAM_ID |
Apple Developer Team ID |
APPLE_ID |
Apple ID email for notarytool authentication |
APPLE_ID_PASSWORD |
App-specific password for notarytool (generate at appleid.apple.com) |
Setup steps:
- Export your Developer ID Application certificate as .p12 from Keychain Access
- Base64-encode it:
base64 -i cert.p12 | pbcopy - Create an app-specific password at https://appleid.apple.com/account/manage
- Add all five secrets in the repository settings