Skip to content

CreatorTrack Personal Contacts <-> Google Contacts sync

URL: https://mkdocs.justinsforge.com/memory/general/reference_creatortrack_contacts_google_sync/

Two-way sync between the Personal Contacts app (crm.people) and Google Contacts, built 2026-07-08 on branch feat/food-ai-contacts-sync, merged to main (c6fdcc1). Faithful analog of the Google Tasks two-way sync (gtasks pattern). Reuses the shared per-user Google connection plumbing (google-connections); does NOT hand-roll auth. [Claude Code]

Files

  • lib/google-contacts.ts , thin People API v1 client (incremental listConnections w/ syncToken, create/update/deleteContact, etag + updateTime cursors, SyncTokenExpiredError on 410).
  • lib/google-contacts-sync.ts , the engine: pull (Google->CT) + push (CT->Google) + deletes both ways, app.google_contacts_links mapping, content_hash echo-suppression BOTH directions, newest-wins conflict (source updateTime vs node updated_at). runGoogleContactsSync (loud) + runGoogleContactsSyncBestEffort (swallows for piggy-backed runs).
  • lib/google-contacts-settings.ts , users.prefs.crm_app.gcontacts_enabled + .gcontacts_state (chosen account + People syncToken cursor + lastSyncAt), sibling-preserving jsonb_set idiom.
  • db/migrations/20260708T190500_..._google_contacts_links_table.sql + db/schema.ts googleContactsLinks (workspace RLS, mirrors google_tasks_links).
  • app/api/apps/crm/route.ts ops: get_gcontacts_settings, set_gcontacts_settings, run_gcontacts_sync.
  • components/apps/crm/ContactsSyncView.tsx , the "Google Sync" rail view (connect account, pick account, enable toggle, Sync now + last-synced).
  • Scope group contacts: [auth/contacts] added to lib/google-connections.ts.

Field mapping (round-trips)

name, nickname, birthday, email, phone, address, notes. CT-only (never touched): relationship, anniversary, lastContacted, interaction timeline. Conflict = last-writer-wins (loser logged as a skip, never silently dropped).

ACTIVATION (required before it works , GCP + DB)

DORMANT until these are done; the code ships gated behind connect + enable so it's safe undeployed. 1. GCP console (project creatortrack-499820, oauth project): enable the People API; add scope https://www.googleapis.com/auth/contacts to the OAuth consent screen; keep the app in Production (Testing = 7-day token expiry). Re-verification may be required for the sensitive contacts scope. 2. Apply the migration to the CreatorTrack DB before the first sync: WORKSPACE_MIGRATIONS_DIR=db/migrations python3 ~/forge/scripts/forge_workspace_migrate.py (idempotent; creates app.google_contacts_links). The settings VIEW loads without it; only a running sync needs the table. 3. In the app: Personal Contacts -> Google Sync -> Connect account -> pick it -> toggle on -> Sync now. First run does a full import (no syncToken); subsequent runs are incremental.

Timer (not yet wired)

v1 is on-demand (Sync now). A scheduled runGoogleContactsSync on a systemd timer (cadence like forge-creatortrack-sales-gmail-sync.timer, every 30 min) is the obvious follow-up once activated.