Sync local files with Neon Postgres records. This extension allows you to upload and download configuration files (or any text content) to/from a PostgreSQL database, preserving comments and formatting by treating content as raw text.
- One-Command Sync: A single
Sync Filecommand auto-picks direction by checking which side changed since this machine last synced the profile. Only local changed → upload; only remote changed → download; both changed → a conflict prompt asks you to pick. Profiles never synced on this machine fall back to comparing localmtimewith remoteupdate_time. - Safe Writes: Uploads only succeed if the remote still holds exactly what you reviewed — if another machine synced in the meantime, nothing is written and you're offered a re-sync. Likewise, a local file edited while the diff was open is never overwritten.
- Multi-Profile Sync: Pick
Sync Multiple Profiles…from the same picker to open a batch page listing each selected profile's direction and added/removed line counts. Adjust direction, open a diff, confirm individually, or hitConfirm Allto commit every pending row in a single atomic transaction.Alt+Atoggles select-all in the multi-pick (respects the active search). - Manual Direction Override: Click the
⇄icon in the diff title bar (or pressAlt+S) to flip the sync direction mid-review. Candidate-side edits (saved or unsaved) are protected with a confirmation before being discarded. - Text-based Sync: Preserves comments, whitespace, and formatting in your files.
- Interactive Diff: Review changes (and edit the candidate side) before confirming with
Alt+Enter. - Smart Language Detection: Diff views respect file associations (e.g., JSONC files display without false syntax errors).
- Secure Storage: Your database connection string is stored securely using VS Code's Secret Storage.
- Profile Management: Manage multiple file-to-record mappings through a theme-aware settings panel with modal-based add/edit and a native file picker — or edit
neon-sync.jsondirectly viaNeon Sync: Open Settings (JSON). - Persistent Profile Order: Most recently used profile order is saved globally and survives
Reload Window. - Server-side Timestamping: Database timestamps (
create_time,update_time) are generated by the database (CURRENT_TIMESTAMP).
The same syncing is available from the terminal, without opening an editor — with the same profiles, the same sync history and the same safety rules:
npm install -g @ciuzaakwong/neon-sync
neon-sync config set-url # stored in the OS keychain
neon-sync # what's out of sync, then offers to apply itScriptable too (neon-sync sync --yes --json, meaningful exit codes). See cli/README.md.
Ensure your PostgreSQL database has a table with the following schema (you can customize the table name in your profiles):
CREATE TABLE IF NOT EXISTS json_records (
id TEXT PRIMARY KEY,
data TEXT,
create_time TIMESTAMP,
update_time TIMESTAMP
);Note: The data column must be of type TEXT to support raw content sync. (json columns keep text verbatim but reject comments; jsonb rewrites content into Postgres's canonical form, so a file uploaded there never compares identical to the stored row afterwards.)
Each profile needs its own local file — two profiles pointing at the same file are refused at sync time.
Run Neon Sync: Open Settings to open the configuration panel, then:
- Paste your PostgreSQL connection string into the Connection field. It auto-saves on blur into VS Code's Secret Storage.
- Click
+ Addunder Profiles to create a profile. Required fields: name, local file path (useBrowse…to pick), record ID, and table name (defaults tojson_records).
Connection string format:
postgres://user:password@host:port/dbname
The extension uses HTTP transport (@neondatabase/serverless) only (no TCP mode).
If you trigger a sync without a connection string configured, the error toast will offer an Open Settings button that jumps straight to the Connection field.
Profiles live in one per-user file shared by every editor with this extension installed (VS Code, Cursor, …) and by the neon-sync CLI:
| Profiles | Sync history (per machine) | |
|---|---|---|
| macOS / Linux | ~/.config/neon-sync/neon-sync.json |
~/.local/state/neon-sync/sync-state/ |
| Windows | %APPDATA%\neon-sync\neon-sync.json |
%LOCALAPPDATA%\neon-sync\sync-state\ |
These locations are fixed (environment variables such as XDG_CONFIG_HOME are ignored) so that every tool reads the same history. Open the file via Neon Sync: Open Settings (JSON); the settings panel shows its path too. The file may be a symlink (e.g. managed by a dotfiles tool) — writes go to its target.
Upgrading: on first launch, each editor moves its own profiles (previously kept in that editor's private storage) into the shared file. Profiles with the same name keep the shared version, profiles pointing at a file another profile already uses are skipped, and a notification lists what was added. The old per-editor file is kept as a backup (any legacy plaintext connection string is moved into secure storage first). Downgrading the extension afterwards reads that old backup; edits made there are not merged back.
File paths may start with ~/. Relative paths are resolved against the first workspace folder (the CLI has no workspace: it only syncs them with --base <dir>), so prefer ~/… or absolute paths — Browse… stores those.
Example neon-sync.json:
{
"profiles": [
{
"name": "My Config",
"filePath": "~/.config/app/settings.json",
"id": "app-settings",
"tableName": "json_records"
},
{
"name": "Env Variables",
"filePath": "~/projects/app/.env",
"id": "env-vars",
"tableName": "app_config"
}
]
}For JSON/JSONC profiles you can list keys that should be hidden from the diff and preserved on the target side on confirm. This is useful for machine-specific or transient keys (themes, machine IDs, locally chosen font sizes) that you don't want propagating across machines.
Add excludeKeys to a profile (via the settings panel's Add/Edit modal → Advanced, or by editing neon-sync.json directly):
{
"profiles": [
{
"name": "App Settings",
"filePath": "~/.config/app/settings.json",
"id": "app-settings",
"tableName": "json_records",
"excludeKeys": [
"theme",
"window.zoom",
"telemetry.machineId"
]
}
]
}Path syntax:
- Dot-separated string. A dot either separates levels or is part of a key name, whichever the file has:
editor.fontSizematches VS Code's flat{ "editor.fontSize": 14 }as well as{ "editor": { "fontSize": 14 } }, anda.b.calso matches{ "a.b": { "c": … } }and so on. Only whole key names match (editor.fontdoesn't matcheditor.fontSize). If a file has several forms, all of them are excluded, and each side keeps its own values in its own form. - Multiple paths supported; multi-level paths supported.
- Wildcards (
a.*.b), array indices and language-specific blocks such as[json]are not supported yet.
Behavior:
- Diff view: both sides are shown with the listed keys removed. Comments and formatting on remaining keys are preserved.
- Identical check: if the only differences are filtered keys, the sync is treated as "already in sync" and skipped.
- Confirm: each side keeps its own values for the filtered keys — they are spliced back in, verbatim, before writing. An upload writes the remote's values into the remote row and keeps the local file's own values locally. Values for filtered keys edited in the diff editor are overwritten (these keys are "owned" by each side).
- Parse failure: if either side does not parse as JSONC (comments and trailing commas allowed), the sync is aborted with an error. The multi-profile panel keeps the row visible with an inline error.
- No filter: profiles without
excludeKeys(or with an empty list) behave exactly as before — pure raw-text sync.
-
Run
Neon Sync: Sync Fileand select a profile. -
The extension compares local
mtimeagainst the remoteupdate_timeand opens a diff in the picked direction:Local ← Remote(remote is newer, will overwrite local on confirm)Remote ← Local(local is newer, will overwrite remote on confirm)
If the timestamps are ambiguous (see below), a modal asks you to pick
Download (Local ← Remote)orUpload (Remote ← Local)first; dismissing the modal aborts the sync. -
Optionally edit the right-hand side of the diff to fine-tune the final content.
-
Confirm with
Alt+Enter(or the✓button), press the⇄button /Alt+Sto flip direction before confirming, or click the✕button to cancel.
- Run
Neon Sync: Sync Fileand pickSync Multiple Profiles…from the list (its position floats up as you use it). - Check the profiles you want to sync (Space toggles,
Alt+Atoggles select-all for whatever matches the current search). - The batch page fetches all selected records in a single HTTP transaction and lists every row that still needs syncing with its proposed direction and
+added / -removedline counts. Identical profiles are skipped with a notification; missing-both profiles are called out separately. - Per row you can
Swapthe direction, open aDiff(same editor as single-profile sync), orConfirmjust that row.Confirm Allcommits every upload in a single atomic transaction, then writes the local files. Rows marked⚠ conflict(both sides changed) are left out ofConfirm Alluntil you act on them individually. - If any row's remote changed since the page loaded,
Confirm Allwrites nothing and offersReload. A row whose local file changed since loading is skipped the same way. - If a local write fails after a successful remote commit, the affected row stays visible with a
remote committedbadge so a retry only re-runs the local write — it won't re-upload or bumpupdate_time.
After every successful sync, the extension records (per machine, per profile) a fingerprint of the content both sides agreed on. The next sync judges each side against that record:
| Local since last sync | Remote since last sync | Picked direction |
|---|---|---|
| unchanged | changed | Local ← Remote |
| changed | unchanged | Remote ← Local |
| changed | changed | conflict — you pick |
| identical now | identical now | no sync |
With excludeKeys, changes confined to the filtered keys don't count. If one side is missing, the other side is copied over (Local ← Remote when the file is missing, Remote ← Local when the record is).
No sync history yet (first sync on this machine, or the profile's excludeKeys changed since): the newer timestamp wins — local mtime vs remote update_time — unless they are within 5 seconds of each other or either is missing, in which case you're asked to pick (Download (Local ← Remote) / Upload (Remote ← Local); dismissing aborts). Local mtime comes from the OS clock while update_time comes from the Neon server, so small gaps are unreliable. Profiles that are already in sync get a history record the first time they're checked.
In every case the diff opens in the chosen direction and you can still swap before confirming. Sync history lives on each machine (see the table above), never in the database.
This extension was originally designed to synchronize Google Antigravity configurations.
Models used to build this project: Gemini 3 Pro did the initial scaffolding; ongoing maintenance is mostly Claude Opus / Sonnet, with GPT (Codex) handling review passes.

