Skip to content

feat: add bidirectional tag sync#120

Merged
tomymaritano merged 9 commits into
developfrom
feature/tag-sync
Mar 11, 2026
Merged

feat: add bidirectional tag sync#120
tomymaritano merged 9 commits into
developfrom
feature/tag-sync

Conversation

@tomymaritano

Copy link
Copy Markdown
Collaborator

Summary

  • Add tagSyncLog table to server schema (Drizzle/Turso)
  • Add local SQLite migration: UUID column + sync tracking (needs_sync, local_version) + auto-tracking triggers
  • Add GET /sync/tags and POST /sync/tags API endpoints (cursor-based, Pro subscription gated)
  • Add tag sync repository methods: getTagsPendingSync, markTagAsSynced, upsertTagFromRemote (dedup by UUID then name), deleteTagByUuid
  • Add pullTagChanges/pushTagChanges to API client
  • Integrate tag sync into syncNow() cycle (pull tags → push tags, after note sync)
  • Wire up IPC bridge (sync:pullTags, sync:pushTags) and preload API

Design decisions

  • Tags are NOT encrypted — they're lightweight metadata (name + color)
  • Dual-key identity: UUID for cross-device sync, name for dedup on pull
  • Only manual tags + colors sync — content-extracted #hashtags derive from markdown
  • Tag sync failures don't fail overall sync — tags are secondary to notes

Test plan

  • pnpm typecheck passes (all 3 targets: main, preload, renderer)
  • pnpm lint clean
  • pnpm format:check clean
  • pnpm test passes (all 14 tasks)
  • Manual: create tag on device A → sync → verify appears on device B
  • Manual: rename/recolor tag → sync → verify updates propagate
  • Manual: delete tag → sync → verify removed on other device
  • Manual: create same-name tag on two devices → sync → verify dedup (UUID adopted)

🤖 Generated with Claude Code

tomymaritano and others added 8 commits March 11, 2026 09:55
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
DatabaseConnection.transaction() already calls the inner fn — no need
for extra () at call site.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 26f2757055

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


const changes = await db
.select()
.from(tagSyncLog)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add migration for tag_sync_log before querying it

These handlers read from tag_sync_log, but this commit only adds the Drizzle schema/type and route code; it does not add a corresponding SQL migration entry under packages/api/drizzle (or a new journal entry). In environments that apply schema changes via pnpm db:migrate, the table will not exist, so /sync/tags requests will fail at runtime with a database error (no such table: tag_sync_log).

Useful? React with 👍 / 👎.

const stmt = this.db.prepare(`
SELECT id, uuid, name, color, local_version
FROM tags
WHERE needs_sync = 1 AND uuid IS NOT NULL

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Assign UUIDs when creating new local tags

Pending tag sync now requires uuid IS NOT NULL, but local tag creation paths still insert tags without a UUID (existing inserts only provide name). After this change, newly created local tags can be marked needs_sync=1 yet never selected for upload, so tags created on one device will not propagate to other devices.

Useful? React with 👍 / 👎.

Comment on lines +45 to +47
local_version = local_version + 1,
needs_sync = 1
WHERE id = NEW.id;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Exclude remote-applied changes from sync-tracking triggers

This trigger marks any tag name/color update as needs_sync=1, including updates applied during pullTags() (upsertTagFromRemote updates these same columns). Because syncNow() then runs pushTags(), pulled remote changes are immediately re-queued for upload, creating echo/conflict loops where tags do not remain in a clean synced state.

Useful? React with 👍 / 👎.


const changes = pending.map(({ tag, localVersion }) => ({
tagId: tag.uuid,
operation: 'update' as const,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Emit delete operations for removed tags

Client-side tag push currently hardcodes every change as operation: 'update'. Since deleted tags are removed from the local tags table before sync, no delete payload can be generated even though the server endpoint supports 'delete', so tag deletions on one device will not be propagated and stale tags remain on other devices.

Useful? React with 👍 / 👎.

# Conflicts:
#	apps/desktop/src/main/services/apiClient.ts
#	apps/desktop/src/main/services/syncService.ts
#	packages/api/src/db/schema.ts
#	packages/api/src/routes/sync.ts
#	packages/storage-sqlite/src/migrations/index.ts
@tomymaritano tomymaritano merged commit 63610e4 into develop Mar 11, 2026
2 of 3 checks passed
@tomymaritano tomymaritano deleted the feature/tag-sync branch March 11, 2026 13:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant