Spread Sync

unlisted

by Yuto Takagi

Embed Google Sheets cells/ranges into Markdown with on-open sync, in-place editing, and explicit write-back.

Updated 2mo agoMIT
View on GitHub

๐Ÿ“Š Spread Sync

Your Google Sheet, inside your Obsidian note.

Embed sheet cells inline. Render ranges as tables. Edit them in place. Push back. All with conflict-aware writes and PKCE OAuth.

License: MIT Obsidian Desktop only Tests Docs

๐Ÿ“– Docs ยท ๐Ÿš€ Install ยท โš™๏ธ Setup ยท ๐Ÿ’ก Usage


โœจ What it does

๐Ÿ”ข Inline cell`spread_sync("id", "Sheet1", "B3")` โ†’ renders as the live value
๐Ÿ“‹ Range table```spread-sync ``` block โ†’ Markdown table, headers/transpose/named ranges supported
โœ๏ธ Edit in placeAdd editable: true โ†’ cells become editable, one button batchUpdates all changes
โœ๏ธ Explicit write```spread-write ``` block + Push this block command
๐Ÿ›ก๏ธ Conflict-awareDrive modifiedTime check before every push, Overwrite/Cancel prompt on mismatch
๐Ÿ” PKCE OAuthBring your own Google Cloud Desktop client. Refresh token encrypted via safeStorage
โšก Smart cachingLRU + debounced persist, stale-while-revalidate, manual refresh commands
๐ŸŒ Resilient429 backoff, offline-aware, dedicated error badges (no access, Excel format, etc.)

๐Ÿš€ Install

cd /path/to/your/vault/.obsidian/plugins
git clone https://github.com/yut0takagi/obsidian-spread-sync.git spread-sync
cd spread-sync && npm install && npm run build

Then in Obsidian: Settings โ†’ Community plugins โ†’ enable Spread Sync.

โš™๏ธ Setup (5 min)

Spread Sync uses your own Google Cloud OAuth credentials. You stay in control of scopes and quota.

Step 1 โ€” Create a Google Cloud project

Open Google Cloud Console โ†’ create or select a project.

Step 2 โ€” Enable APIs

APIs & Services โ†’ Library โ†’ enable:

  • Google Sheets API
  • Google Drive API
Step 3 โ€” Configure OAuth consent screen
  • User type: External
  • Add scopes: .../auth/spreadsheets + .../auth/drive.metadata.readonly
  • Publishing status: In production (avoids the 7-day refresh-token expiry of "Testing" mode)
Step 4 โ€” Create OAuth client

Credentials โ†’ Create credentials โ†’ OAuth client ID โ†’ Application type: Desktop app. Copy both Client ID and Client secret.

Note: Google's docs explicitly say Desktop OAuth client secrets are not actually confidential (source). Storing it in your local Obsidian settings is consistent with how the official Google libraries handle it.

Step 5 โ€” Paste into Obsidian

Settings โ†’ Spread Sync โ†’ OAuth credentials section. Paste both values. Click Sign in with Google. Done.

๐Ÿ’ก Usage

Inline cell

The current store count is `spread_sync("<spreadsheet-id>", "Sheet1", "B3")`.

Range as a table

```spread-sync
id: <spreadsheet-id>
sheet: Sheet1
range: A1:D10
header: true        # optional, default true
transpose: false    # optional, default false
```

Editable table โœจ

```spread-sync
id: <spreadsheet-id>
sheet: Sheet1
range: A1:D10
editable: true
```
Click any cell to edit.Dirty cells highlight yellow.Push button at the bottom batches all dirty cells into one values:batchUpdate.

Whole sheet

```spread-sync
id: <spreadsheet-id>
sheet: Sheet1
```

Named range

```spread-sync
id: <spreadsheet-id>
sheet: Sheet1
named: MonthlyKPI
```

Aliases

Set kpi โ†’ <long-spreadsheet-id> in settings, then reference as "@kpi":

```spread-sync
id: "@kpi"
sheet: Sheet1
range: A1:C5
```

Explicit write (one-shot)

```spread-write
id: <spreadsheet-id>
sheet: Sheet1
target: B3
value: 42
mode: replace       # or "append"
```

Place cursor inside the block โ†’ Command Palette โ†’ Spread Sync: Push this block.

For range writes:

target: A1:B2
value:
  - [1, 2]
  - [3, 4]

๐ŸŽ›๏ธ Commands

CommandDescription
Spread Sync: Refresh current fileInvalidate cache for the active file, force re-fetch
Spread Sync: Refresh all open filesSame, for every open markdown leaf
Spread Sync: Refresh by spreadsheetPrompt for an id / URL / @alias and invalidate all its cache
Spread Sync: Push this blockSend the current spread-write block to Sheets

๐Ÿšจ Error badges

BadgeMeaning
โš  re-authToken expired / not signed in โ€” click to re-sign-in
โš  no access403 / 404 from Sheets โ€” check sharing or API enablement
โš  Excel formatSheet is a .xlsx uploaded to Drive โ€” convert to native Google Sheets
โš  syntax errorBlock YAML / inline call is malformed (tooltip shows the parser error)
โš  range errorInline syntax got a multi-cell range, or editable got an invalid range
โš  fetch failedGeneric API error โ€” hover for full message
โš  stale NmLast fetch failed; showing cached value N minutes old
โš  offlineNetwork is down

๐Ÿ—๏ธ Architecture

Four isolated layers:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Renderer   inline / block postprocessors    โ”‚
โ”‚             editable table widget            โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  Auth       OAuth (PKCE) ยท safeStorage       โ”‚
โ”‚             localhost loopback callback      โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  Sheets+Drive   pure HTTP (https.request)    โ”‚
โ”‚                read ยท batchRead ยท write      โ”‚
โ”‚                batchWrite ยท modifiedTime     โ”‚
โ”‚                429 backoff                   โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  Storage    LRU cache ยท debounced persist    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

๐Ÿ“‹ Requirements

  • Obsidian 1.4+
  • Desktop only (Electron safeStorage not available on mobile)
  • Node 18+ for build

๐Ÿšง Limitations

  • editable: true only works with literal range: like A1:B2 (not named: or whole sheet, not transpose: true).
  • No batchGet wiring yet โ€” each inline expression is a separate API request (Phase 2).
  • Push this block uses editor.setValue which resets cursor/undo history (Phase 2: replace with replaceRange).

๐Ÿงช Development

npm install
npm run dev     # esbuild watch mode
npm test        # vitest run โ€” 59 tests across 8 files

Tests cover: parser, id resolver, cache, Sheets client (with nock), Drive client, OAuth client (PKCE + token exchange), TokenStore, table formatter. Renderer / commands / settings UI are verified manually in a real Obsidian vault.

๐Ÿ“„ License

MIT โ€” built by @yut0takagi.

For plugin developers

Search results and similarity scores are powered by semantic analysis of your plugin's README. If your plugin isn't appearing for searches you'd expect, try updating your README to clearly describe your plugin's purpose, features, and use cases.