SeaMeet Desktop is here — Record everything, miss nothing. Download free →

GitHub Sync Setup

Chapter 38: GitHub Sync Setup

GitHub Sync turns your SeaMeet workspace into a private, version-controlled Markdown tree that lives in your own GitHub repository. Every time you edit a note, retitle a recording, or tweak a wiki page, SeaMeet quietly commits the change in the background and pushes it to GitHub. Open SeaMeet on a different laptop and your notes pull down on first launch — no Dropbox in the middle, no Seasalt server in the loop. The repo is yours, the data is yours, and you can browse it directly on github.com if you ever want to.

The model is simple: your GitHub repository is the storage. SeaMeet doesn't run a sync service. It installs a small GitHub App on your account, gets a per-user OAuth token, and uses that token to push and pull Git commits directly. Because the storage is GitHub's free tier — which gives you unlimited private repos — sync is effectively free. And because each SeaMeet workspace is its own repository, you can keep work and personal workspaces fully separated.


Chapter Objectives

After reading this chapter, you will be able to:

  • Install the SeaMeet GitHub App on your account or organization
  • Authorize SeaMeet over OAuth and let it create a private sync repository for you
  • Read the sync status bar and understand each state in the sync state machine
  • Recognize when OAuth has auto-refreshed in the background and when you need to re-authorize manually
  • Resolve a sync conflict when the same note has been edited on two devices
  • Tune the Sync Settings panel (auto-sync, sync interval, sync on startup)
  • Troubleshoot the most common failure modes

Prerequisites

Before you start, make sure you have:

  • A GitHub account — free tier is fine; you don't need GitHub Pro
  • An active sync entitlement — included with BYOK Pro and Sync Pro tiers. If your billing tier doesn't include sync, the Sync Settings screen will show a "Subscribe to enable sync" banner. See Chapter 35: Subscription & Billing for plan details.
  • A workspace open in SeaMeet — sync attaches to the active workspace, not the app as a whole. If you have multiple workspaces, each one is its own GitHub repository.
  • Working internet access — the setup flow opens GitHub in your default browser

One repo per workspace. SeaMeet creates a separate private repository for each workspace you connect. This keeps work notes and personal notes cleanly isolated and means you can disconnect one without touching the other.


Step-by-Step Setup

The first-time setup takes about 90 seconds: authorize SeaMeet on GitHub, let it create your repo, wait for the initial upload, and you're done.

Step 1: Open Sync Settings

  1. Click the Settings gear icon
  2. Open the Sync tab in the left rail
  3. You'll see the "Sync with GitHub" header card

If you don't see the Sync tab, your subscription tier doesn't include sync — see Chapter 35 to upgrade.

If you see a banner that reads "Back up your notes to a private GitHub repo with BYOK Pro", you have a free account. Click the banner to open the paywall, or skip the rest of this chapter until you've subscribed.

Step 2: Click "Set Up GitHub Sync"

The Sync Settings empty state shows three benefit tiles ("Free & private", "Effectively unlimited", "One repo per workspace") and a primary Set Up GitHub Sync button at the bottom.

Click it. Your default browser opens to a GitHub authorization page.

Step 3: Install the SeaMeet GitHub App (one-time)

The first time you authorize SeaMeet, GitHub will ask you to install the SeaMeet GitHub App on your account. This is a one-time step per GitHub account (or per organization, if you want to sync to an org-owned repo).

On the GitHub install page:

  1. Choose the account or organization you want SeaMeet to write to
  2. Choose either "All repositories" or "Only select repositories" — the SeaMeet App needs contents: write and administration: write permissions so it can create the sync repo on your behalf
  3. Click Install & Authorize

GitHub bounces you back to SeaMeet. The Sync Settings panel now shows:

┌──────────────────────────────────────────────────────┐
│   ✓ Authorized                                       │
│   ⟳ Creating repository & uploading...               │
└──────────────────────────────────────────────────────┘

Why a GitHub App and not a plain OAuth token? GitHub Apps have fine-grained per-repo permissions and rotating refresh tokens. SeaMeet only ever has write access to the repos you explicitly chose, and the OAuth access token expires every 8 hours — even if it leaked, the window would be small. (See the OAuth Auto-Refresh section below for how SeaMeet keeps that invisible to you.)

Step 4: Repository creation (automatic)

SeaMeet now calls the GitHub API on your behalf and:

  1. Creates a private repo named after your workspace (e.g. seameet-work-notes)
  2. Initializes a local Git repo inside your workspace folder if one doesn't already exist
  3. Runs git init, writes a .gitignore that excludes audio/video/screenshot files, and makes an initial commit
  4. Adds the new GitHub repo as origin
  5. Pushes the initial commit

You'll see the status bar at the bottom of the sidebar transition through:

Creating repository...  →  Syncing...  →  Synced · just now

That's it. Your workspace is now backed by GitHub.

Step 5: First sync runs automatically

From now on, SeaMeet auto-commits every 5 minutes (by default) whenever you have unsaved changes, and pushes to GitHub immediately after each commit. On the next launch of SeaMeet, the app pulls from GitHub before you even start editing — so changes made on another device show up the moment the app opens.


The Sync State Machine

SeaMeet's SyncManager runs a small state machine and broadcasts its current state to the renderer. The sidebar SyncStatusBar and the Sync Settings panel both read from this state. Understanding the states helps you read the UI accurately.

                      ┌─────────────┐
                      │    idle     │ ◄──────┐
                      └──────┬──────┘        │
                             │ file change   │
                             ▼               │
                      ┌─────────────┐        │
                ┌────►│   pending   │        │
                │     └──────┬──────┘        │
       file     │            │ commit timer  │
       change   │            ▼               │
                │     ┌─────────────┐        │
                │     │ committing  │        │
                │     └──────┬──────┘        │
                │            ▼               │
                │     ┌─────────────┐        │
                │     │   pushing   │        │
                │     └──────┬──────┘        │
                │            ▼               │
                │     ┌─────────────┐        │
                │     │   synced    │── 5s ──┘
                │     └─────────────┘
                │
                │     ┌─────────────┐
                ├─────┤   pulling   │  (startup or push-rejected)
                │     └─────────────┘
                │
                │     ┌─────────────┐
                ├─────┤  conflict   │  → user resolves → syncing → synced
                │     └─────────────┘
                │
                │     ┌─────────────┐
                ├─────┤   offline   │  → network back → retry
                │     └─────────────┘
                │
                │     ┌─────────────┐
                ├─────┤   timeout   │  → user clicks Retry
                │     └─────────────┘
                │
                │     ┌────────────────┐
                └─────┤ token-expired  │ → re-authorize card
                      └────────────────┘

Here's what each state means in the UI:

StateStatus bar labelWhat it means
idle"Synced · 2 min ago"Nothing pending, last commit was clean
pending"Pending..."You have unsaved changes; commit timer will fire on the next interval
committing"Syncing..."A Git commit is in progress locally
pushing"Pushing..."Local commit complete; pushing to GitHub
pulling"Pulling..."Either startup sync, or push got rejected and we're pulling first
syncing"Syncing..."Finishing a rebase after conflict resolution
synced"Synced · just now"Just-completed success; shown for 5 seconds, then returns to idle
conflict"Sync conflict"Local and remote both edited the same file. UI surfaces the conflict resolution flow.
error"Sync error"A non-network, non-conflict failure (e.g. git failure). Auto-retries with exponential backoff up to 5 times.
offline"Offline"Network unreachable. Will retry on the next file change or after 60 seconds.
timeout"Sync timed out"A git command took longer than 60 seconds (see Sync Timeout Protection below)
token-expired"Re-authorize"OAuth refresh failed and the user must re-authorize manually
installing-app"Installing GitHub App..."Waiting for the user to finish the GitHub App install step in their browser
creating-repo"Creating repository..."Setup flow is creating the GitHub repo and pushing the initial commit

If the status bar is showing a spinner, that's normal background activity. If it's amber or red, see the Troubleshooting section below.


OAuth Auto-Refresh

GitHub Apps issue user access tokens that expire every 8 hours, plus a refresh token that's valid for 6 months. SeaMeet manages this for you so you almost never have to think about it.

How it works:

  1. When SeaMeet is about to push or pull, SyncManager._refreshTokenIfNeeded() checks whether the current token expires within the next 5 minutes.
  2. If yes, SeaMeet calls GitHub's /login/oauth/access_token endpoint with the stored refresh token. GitHub returns a new access token and rotates the refresh token. SeaMeet writes both back to the encrypted token file on disk.
  3. The Git command then runs with the freshly-refreshed token. You see nothing on screen — the whole refresh takes well under a second.

Transient failure handling: If the refresh call fails because the network is flaky or GitHub returned a 5xx (a REFRESH_NETWORK_ERROR), SeaMeet does not throw away the existing token. Instead the sync goes into the offline state and tries again later. This matters because the existing token may still be valid for several more minutes — discarding it on a transient network blip would log you out of sync unnecessarily.

Concurrent refresh coalescing: GitHub rotates the refresh token every time you use it, so two concurrent refreshes would race and one would invalidate the other. SeaMeet collapses concurrent refresh calls into a single in-flight Promise (_refreshPromise) so startup-sync and push-with-retry never fight over the rotating token.

When refresh truly fails: If GitHub returns REFRESH_TOKEN_EXPIRED — meaning the 6-month refresh token has expired or been revoked — SeaMeet:

  1. Deletes the encrypted token file from disk
  2. Enters the token-expired terminal state
  3. Shows an amber card in Sync Settings:
┌──────────────────────────────────────────────────────┐
│ ⚠  GitHub authorization has expired. Please           │
│    re-authorize to resume syncing.                    │
│                                                       │
│  [ ↻ Re-authorize GitHub ]                            │
└──────────────────────────────────────────────────────┘

Click Re-authorize GitHub. You're sent back through the OAuth flow — but because the GitHub App is already installed, it's a single click to confirm and you're done in about 10 seconds. SeaMeet then strips any stale token from your local git config (the old token may be embedded in origin's URL) before the next push, so you don't loop on a dead credential.


Sync Timeout Protection

Git commands can hang forever if a network goes silent mid-transfer — a TCP socket may sit in a half-open state without ever raising an error. SeaMeet wraps every Git push and pull in a 60-second AbortController timeout.

If a push or pull doesn't complete in 60 seconds, SeaMeet:

  1. Aborts the git subprocess
  2. Enters the timeout state
  3. Shows an amber card in Sync Settings:
┌──────────────────────────────────────────────────────┐
│ ⚠  Sync timed out. This may be caused by a network   │
│    issue or expired GitHub authorization.             │
│                                                       │
│  [ ↻ Retry Sync ]   [ Re-authorize GitHub ]           │
└──────────────────────────────────────────────────────┘

Retry Sync runs commitNow again. This is the right button to click if you think your network just dropped briefly. Re-authorize GitHub runs the OAuth flow again; click this if the timeout keeps recurring, which usually means your token is the problem rather than the network.

The status returns to idle automatically after about 30 seconds even if you don't click anything — sync will retry on the next file change.


Conflict Resolution

A conflict happens when the same file has been edited on two devices since the last sync. Example: you wrote a paragraph in meetings/2026-06-03-standup.md on your laptop, then later edited the same paragraph on a friend's machine. When the second device pushes, GitHub rejects the push because the histories diverge.

SeaMeet handles this by:

  1. Pulling the remote changes with a rebase
  2. Detecting the merge conflict markers
  3. Entering the conflict state
  4. Surfacing the conflict in the sidebar status bar (which becomes clickable) and in the Sync Settings conflict panel

The Conflict Resolution Modal

Clicking the status bar when sync is in conflict opens the Conflict Resolution Modal:

┌──────────────────────────────────────────────────────────┐
│  ⚠  Sync Conflict                    1 / 2          [×]  │
├──────────────────────────────────────────────────────────┤
│                                                          │
│  "2026-06-03-standup.md" was changed on another device.  │
│                                                          │
│  ┌────────────────────────┐  ┌────────────────────────┐  │
│  │ ✎  Edit manually       │  │ ✦  Agent merge         │  │
│  └────────────────────────┘  └────────────────────────┘  │
│                                                          │
└──────────────────────────────────────────────────────────┘

You have two options:

Edit manually — opens the conflict file in your system's default editor with Git's standard <<<<<<< / ======= / >>>>>>> markers in place. Edit the file to resolve them, save, then come back to SeaMeet and click Done. SeaMeet stages the resolved file and continues the rebase.

Agent merge (BYOK Pro only, requires AI Agent configured) — sends both versions to your configured AI Agent (GitHub Copilot or Claude Code, see Chapter 36: BYOK Setup Guide) and asks it to produce a merged version that keeps both sides' intent. The agent's output is staged automatically; you'll see a "Finishing sync..." spinner while the rebase completes.

If there are multiple conflicts at once, the modal shows a counter (1 / 2, 2 / 2) and chevron arrows in the Sync Settings conflict panel so you can step through them one at a time.

"Keep mine" / "Keep theirs" / "Keep both" options come up as natural choices when you're inside the manual editor: leave only your version's text between the markers, only the remote version's text, or interleave both. The Agent merge button effectively does "keep both, intelligently."

What if you make a mistake? Sync conflicts are non-destructive. Git keeps the original "ours" and "theirs" content in the index, and getConflictVersions(filePath) can retrieve either at any point during resolution. You won't accidentally lose work.


The Sync Settings Panel

Once you're connected, Sync Settings (Settings → Sync) gives you a single screen with everything in one place:

┌─────────────────────────────────────────────────────────────┐
│  ✓ Sync & Backup                                            │
│  Your notes and wiki are automatically synced to GitHub.    │
│  Audio recordings, video recordings, and screenshots are    │
│  not synced.                                                │
├─────────────────────────────────────────────────────────────┤
│  ⓘ  https://github.com/you/seameet-work-notes                │
│  🕒 Last sync: 4 minutes ago                                 │
├─────────────────────────────────────────────────────────────┤
│  Auto-sync                                       [ ON  ◉ ]  │
│  Sync interval                                  [ 5 min ▾ ] │
│  Branch                                              main   │
│  Last synced                                  4 minutes ago │
├─────────────────────────────────────────────────────────────┤
│  Auto-sync on changes                            [ ON  ◉ ]  │
│  Sync on startup                                 [ ON  ◉ ]  │
├─────────────────────────────────────────────────────────────┤
│  [ ⤓ Sync Now ]                                             │
├─────────────────────────────────────────────────────────────┤
│  [ ↗ View on GitHub ]    [ Disconnect ]                     │
└─────────────────────────────────────────────────────────────┘

The controls, top to bottom:

  • Auto-sync — master toggle for the background commit timer. Off means SeaMeet only commits when you click Sync Now.
  • Sync interval — how often the commit timer fires. Choices are 1 / 5 / 10 / 30 minutes. Default is 5 minutes. A 1-minute interval is fine for active note-taking; 30 minutes is good for low-edit workspaces where you'd rather batch commits.
  • Branch — the Git branch SeaMeet syncs to. Almost always main. Read-only in the UI.
  • Last synced — relative timestamp of the last successful push.
  • Auto-sync on changes — when on, pushes happen immediately after each local commit (the typical case). When off, SeaMeet commits locally but only pushes when you click Sync Now. Useful if you want to batch pushes (e.g. on a metered connection).
  • Sync on startup — when on, SeaMeet pulls from GitHub the moment the app launches. Recommended unless you have a very slow connection.
  • Sync Now — force an immediate commit-and-push cycle. Doesn't wait for the timer. Useful before closing your laptop.
  • View on GitHub — opens the repo in your browser.
  • Disconnect — clears the stored OAuth token, deletes the encrypted token file, and removes origin from your local Git repo. Your local Markdown files and Git history are untouched — only the link to GitHub is severed.

What Gets Synced (and What Doesn't)

This is documented in the part index, but it's worth repeating here:

SyncedNot synced
Notes (.md files)Audio recordings (.webm, .mp3)
Wiki pages (.md files)Video recordings (.webm, .mp4)
Recording manifests (metadata JSON)Screenshots (.png, .jpg)
AI summaries and transcriptsCached AI generations
Wikilink graphApp settings
.gitignoreEditor scratch state

The .gitignore SeaMeet writes excludes all of the large binary categories. This is intentional: GitHub's free private repos cap at 1 GB, and a 30-minute video would eat that on its own. SeaMeet keeps media on your local disk and syncs only the lightweight Markdown.

If you want full media sync, that's the Sync Pro tier — see Chapter 35.


Troubleshooting

"Authorization failed" right after clicking Set Up GitHub Sync

Causes and fixes:

  • Pop-up blocker — if no browser window opened, your OS or browser may have blocked the launch. Click Set Up GitHub Sync again and confirm any pop-up prompts.
  • You denied the GitHub App permissions — go back through the flow and accept the contents: write + administration: write scopes. Without administration: write SeaMeet can't create the repo for you.
  • State mismatch error — close any other SeaMeet windows that may be racing on the same OAuth flow, then retry.

"Repo permissions" error during setup

The error reads something like "GitHub App has 'contents: read' permission, but needs 'contents: write'." This means you installed an older version of the App.

Fix:

  1. Open github.com/settings/installations
  2. Find SeaMeet in the list and click Configure
  3. Under Repository permissions, change Contents and Administration to Read and write
  4. Save, then uninstall and reinstall the app — GitHub only applies new permissions on reinstall
  5. Re-run Set Up GitHub Sync in SeaMeet

"Could not reach GitHub" / sync stuck on "Offline"

Network failure. SeaMeet will retry automatically when:

  • You make any file change (triggers an immediate retry attempt)
  • 60 seconds elapse from the last retry

If your network is back and sync still shows Offline after a couple of minutes:

  1. Click Sync Now to force a retry
  2. If that times out: check your firewall isn't blocking github.com
  3. As a last resort, restart SeaMeet — the startup sync will fire on launch

Sync stuck on "Pending"

This usually means the auto-commit timer hasn't fired yet. By default it fires every 5 minutes. Don't want to wait? Click Sync Now.

If Sync Now doesn't move sync past Pending, check:

  • Are you on a billing tier that includes sync? Open Sync Settings — if you see the subscribe banner, sync is disabled at the entitlement layer.
  • Are there uncommitted changes outside the Markdown tree? The auto-commit timer only fires when _pendingChanges is non-empty.

"I see the token-expired card"

This is the expected end of the OAuth refresh lifecycle — your 6-month refresh token expired, or you revoked the GitHub App, or you changed your GitHub password (which invalidates all OAuth tokens).

Fix: click Re-authorize GitHub in the card. The flow is the same as initial setup but skips the GitHub App install step (you already have it installed). Takes about 10 seconds.

Conflict resolution panel won't close

The panel only closes once all conflicts in the list are resolved. If you see the counter (1 / 3), use the chevron arrows in the Sync Settings conflict panel to step through the remaining files. Each one needs an explicit resolution (manual edit + Done, or Agent merge).

If the panel insists there are conflicts but you don't see any markers in your files, click Sync Now — a stale conflict state will get cleared on the next successful sync attempt.

"Push rejected" loops

If sync alternates between pushing and pulling repeatedly, GitHub is rejecting your pushes because the remote keeps moving ahead. This can happen if:

  • Another SeaMeet instance is running against the same repo and pushing faster than this one can catch up — close the other instance.
  • Someone (or some tool) is committing to the GitHub repo from outside SeaMeet — pause the external tool until your local clone catches up.

Security Notes

Where the OAuth token is stored: in your OS keychain via Electron's safeStorage API. On Windows that's DPAPI, on macOS it's Keychain Services, on Linux it's libsecret. The encrypted blob is written to <userData>/github-token.enc with file mode 0o600. The file holds the access token, the refresh token, the expiry timestamp, and the GitHub App's client ID, client secret, and slug.

What's not stored in argv: the OAuth token is never passed as a -c http.extraHeader=... argument to git, because argv is visible to other users via ps aux. Instead the Authorization header is injected through the GIT_CONFIG_PARAMETERS environment variable, which is private to the process. Other users on the same machine can't see your token.

What ends up on disk in the repo: plain Markdown files. No tokens, no secrets, no credentials. The .gitignore excludes media files, but you should still avoid pasting secrets into note bodies — those will sync.

Revoking access: to immediately cut off SeaMeet's access to a repo, uninstall the SeaMeet GitHub App at github.com/settings/installations. The next sync attempt will fail with token-expired and SeaMeet will clear its local token. Your local notes are untouched.


Quick Reference

┌─────────────────────────────────────────────────────────────┐
│                   GITHUB SYNC                               │
│                   Quick Reference                           │
├─────────────────────────────────────────────────────────────┤
│  Open Sync Settings    │ Settings → Sync                    │
│  First-time setup      │ Click "Set Up GitHub Sync"         │
│  Required scopes       │ contents: write, admin: write      │
│  Repo naming           │ One private repo per workspace     │
├─────────────────────────────────────────────────────────────┤
│  Default sync interval │ 5 minutes (1 / 5 / 10 / 30)        │
│  Force a sync          │ "Sync Now" button                  │
│  Auto-sync on push     │ On (toggle in Sync Settings)       │
│  Sync on startup       │ On (toggle in Sync Settings)       │
├─────────────────────────────────────────────────────────────┤
│  OAuth refresh margin  │ 5 minutes before expiry            │
│  Git command timeout   │ 60 seconds (AbortController)       │
│  Auto-retry limit      │ 5 attempts, exp. backoff           │
│  Token storage         │ OS keychain (safeStorage)          │
├─────────────────────────────────────────────────────────────┤
│  Conflict UI           │ Click status bar when in conflict  │
│  Manual resolve        │ Edit markers → Done                │
│  Agent merge           │ BYOK Pro + AI Agent configured     │
├─────────────────────────────────────────────────────────────┤
│  Disconnect            │ Sync Settings → Disconnect         │
│  Revoke from GitHub    │ github.com/settings/installations  │
│  View repo             │ "View on GitHub" link              │
└─────────────────────────────────────────────────────────────┘

Last updated: 2026-06-04

Chapter 37: GitHub Copilot Provider | (end of manual — return to Index)

Published: