npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@salmi_dev/notion-sync

v1.4.2

Published

Two-way sync between a local codebase and Notion pages: incremental, resumable, rate-limit aware.

Readme

@salmi_dev/notion-sync — a CLI that mirrors a local codebase into Notion pages and keeps it in sync incrementally, with built-in secret protection.

Features

  • Incremental sync — SHA-256 fingerprints with a size/mtime fast path, so a second run only touches what changed.
  • Move and rename detection — a relocated file keeps its page instead of being deleted and recreated.
  • Orphan cleanuparchive, delete or off.
  • Secret scanning before upload with four policies: block, warn, redact, off.
  • Ignore rules — honors .gitignore and .notionignore, plus non-overridable protected excludes for .env, keys and credentials.
  • Real Markdown rendering into Notion blocks; every other file becomes syntax-highlighted code blocks.
  • Rate limiting and retries with exponential backoff and retry-after support.
  • Conflict detection when a page was edited by hand inside Notion.
  • Preview mode--dry-run plus machine-readable JSON reports for CI.
  • Live watch mode with debounce.
  • Reverse syncpull brings edits made inside Notion back to disk, byte for byte, with backups, conflict detection and a unified diff preview.

Install

npm install -g @salmi_dev/notion-sync

# or as a project dev dependency
npm install -D @salmi_dev/notion-sync

Requires Node.js 18.17 or newer.

Quick start

  1. Create an internal integration in Notion settings and copy its token.
  2. Open the page that will host the project, then Share and invite the integration with Can edit access.
  3. Run:
notion-sync init
cp .env.example .env      # then fill in the values
notion-sync doctor
notion-sync sync --dry-run
notion-sync sync

Run init inside the project you want to upload — the config file, .notionignore and .env.example belong to that project, not to the tool.

Commands

| Command | Purpose | | --------------------------- | ------------------------------------------------------------ | | notion-sync init [root] | Create the config file, .notionignore and .env.example | | notion-sync doctor [root] | Check Node version, token, parent page access and state file | | notion-sync sync [root] | One full synchronization | | notion-sync pull [root] | Apply edits made in Notion back to local files | | notion-sync watch [root] | Sync automatically on every save | | notion-sync status | Local summary, no network calls | | notion-sync diff | Show what the next sync would change | | notion-sync diff --remote | Show what a pull would bring back from Notion | | notion-sync prune | Clean up orphan pages only |

Common flags

| Flag | Effect | | ----------------------- | ------------------------------------------- | | --dry-run | Full preview without writing anything | | --force | Re-upload everything and override conflicts | | --only <glob...> | Restrict the run to matching files | | --prune <mode> | archive, delete or off | | --concurrency <n> | Parallel workers, 1 to 16 | | --json <file> | Write a JSON report | | --profile <name> | Independent profile, e.g. staging | | --verbose / --quiet | Log level |

Pull (reverse sync)

Edit a code page inside Notion, then bring the change back to your machine:

notion-sync pull --dry-run        # unified diff, writes nothing
notion-sync pull                  # apply the safe changes
notion-sync pull --strategy file  # write conflicts to NAME.notion.EXT
notion-sync diff --remote         # CI gate: non-zero when Notion drifted

Rules the pull follows:

  • Only files already tracked in the state file are touched. A pull never creates, moves or deletes anything on its own.
  • Only pages rendered as code blocks can be restored byte for byte. Markdown pages, truncated pages, binary placeholders and pages with hand-added blocks are skipped and listed in the summary.
  • Every overwritten file is copied into .notion-sync/backups/TIMESTAMP first, and the five most recent runs are kept.
  • A file changed locally since the last sync, or holding uncommitted git changes, is reported as a conflict and left alone unless you choose a strategy.
  • Line endings and the trailing newline of the local file are preserved, so a pull never produces a whole-file diff in git.

| Pull flag | Effect | | -------------------- | ------------------------------------------------------------------ | | --dry-run | Print the diff, write nothing | | --deep | Read every page instead of trusting last edited time | | --strategy skip | Default: report conflicts and change nothing | | --strategy remote | Notion wins, the local file is overwritten after a backup | | --strategy local | Disk wins, the page is left for the next sync to fix | | --strategy file | Write the Notion version next to the file for manual merging | | --allow-new | Recreate a tracked file that was deleted locally | | --allow-markdown | Also pull markdown pages, which are not round-trip safe | | --keep-backups <n> | How many backup folders to keep, default 5 | | --no-git-check | Stop treating uncommitted files as local changes | | --no-index | Read metadata one page at a time instead of using the search index |

Page timestamps are read from the Notion search index, 100 pages per request, so checking a 129-file project costs about three requests instead of 129. Content is fetched only for the pages that actually changed. Use --no-index if the index looks stale, and --deep when you want every page re-read and compared.

Live two-way watch

notion-sync watch uploads local changes as you save them. Add --pull and it also polls Notion, so an edit made in the browser lands on disk without running a command.

notion-sync watch ./my-project --pull
notion-sync watch ./my-project --pull --pull-interval 30
notion-sync watch ./my-project --pull --strategy remote

Or turn it on permanently in the config file:

{
  "watchPull": true,
  "watchPullIntervalMs": 60000
}

Three rules keep the two directions from feeding each other:

  • Uploads and polls run one at a time, never in parallel, because both take the state file lock.
  • A file written by a pull is ignored by the file watcher exactly once, so it is not pushed straight back.
  • A page counts as edited only when its timestamp is strictly newer than the one recorded right after your own upload. Notion's search index is eventually consistent, so a stamp that goes backwards is a stale read, not a remote edit.
  • A file written by a pull is recognised by its content, not by counting file-watcher events, so a single write cannot leak a second event back into the upload queue.

A poll costs about three requests, so the default one-minute interval is roughly 180 requests an hour, well inside the rate limit.

Configuration

notion-sync.config.json at the root of the project being uploaded:

{
  "root": ".",
  "include": ["**/*"],
  "exclude": ["docs/**"],
  "respectGitignore": true,
  "maxFileSizeBytes": 524288,
  "chunkSize": 1800,
  "maxBlocksPerPage": 900,
  "concurrency": 4,
  "requestsPerSecond": 2.5,
  "maxRetries": 5,
  "prune": "archive",
  "onConflict": "skip",
  "secretScan": "block",
  "renderMarkdown": true,
  "includeBinary": false,
  "logLevel": "info"
}

Precedence: defaults → config file → environment variables → CLI flags.

Environment variables: NOTION_TOKEN and NOTION_PARENT_PAGE_ID, loaded from .env in the target project or the current directory.

Security

.env, *.pem, *.key, credentials* and .ssh/** are excluded unconditionally and cannot be re-enabled from the config file. The token is never written to the state file or to reports.