PSA/ee/docs/plans/gmail-pubsub-single-init-refactor.md
Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

7.1 KiB
Raw Permalink Blame History

Gmail Pub/Sub Initialisation Single-Run Refactor Plan

High-level Goal

Prevent multiple initialisations of the Gmail Pub/Sub topic & subscription that currently occur during OAuth callback and on every provider save. Each logical trigger (OAuth callback or an explicit “Refresh Pub/Sub” action) must result in exactly one call to setupPubSub, ensuring Google Cloud does not disable the subscription for excessive creation attempts.


Table of Contents

  1. Background & Current Problem
  2. Target Architecture (diagram & responsibilities)
  3. Phased To-Do List
  4. Acceptance Criteria / Testing
  5. Roll-out Strategy

1. Background & Current Problem

Action flow today when a user connects Gmail via OAuth:

Step Stack Trace Calls to setupPubSub
1 upsertEmailProvider → finalizeGoogleProvider
2 finalizeGoogleProvider → initializeProviderWebhook → GmailWebhookService.setupGmailWebhook
3 UI onSubmit (provider Save) → updateEmailProvider → finalizeGoogleProvider
4 Same nested path as step 2

Result : four attempts to create/modify the exact same topic & subscription Google rejects or eventually stops the subscription.

Root cause is split responsibility: both orchestration (finalizeGoogleProvider) and Gmail layer (GmailWebhookService) attempt to create Pub/Sub resources.


2. Target Architecture

OAuth Callback ─┬─ store tokens
               └─ configureGmailProvider()  ← the *only* place that calls setupPubSub

configureGmailProvider()
  ├─ generatePubSubNames()
  ├─ setupPubSub()   ← single invocation per logical trigger
  └─ GmailWebhookService.registerWatch() (watch only)

UI Save (settings form)
  └─ upsert / update provider (skipAutomation=true)  ← does *not* touch Pub/Sub

Admin “Refresh Pub/Sub”
  └─ configureGmailProvider() (force=true)

Responsibilities

  • configureGmailProvider orchestration; knows tenant / topic naming; idempotent.
  • GmailWebhookService Gmail-specific; never deals with Pub/Sub.
  • setupPubSub low-level GCP logic, unchanged.

Database adds google_config.pubsub_initialised_at timestamp for an extra idempotency guard.


3. Implementation Checklist

Phase 1: Foundation & Database Schema

  • Create database migration to add pubsub_initialised_at TIMESTAMPTZ field to google_email_provider_config table
    • File: server/migrations/20250719133450_add_pubsub_initialised_at_to_google_config.cjs
  • Update GoogleEmailProviderConfig interface to include pubsub_initialised_at?: string
    • File: server/src/components/EmailProviderConfiguration.tsx:73
  • Create server/src/lib/actions/email-actions/configureGmailProvider.ts
    • Copy logic from finalizeGoogleProvider (emailProviderActions.ts:280-337)
    • Add force?: boolean parameter for admin refresh actions
    • Implement idempotency: return early if pubsub_initialised_at < 24h old and !force
    • Call setupPubSub() with existing interface
    • Call new GmailWebhookService.registerWatch() (not setupGmailWebhook)
    • Update pubsub_initialised_at = NOW() after successful Pub/Sub setup
  • Export configureGmailProvider from email-actions index barrel if present (no index barrel exists)

Phase 2: Trim GmailWebhookService

  • Remove setupPubSub import and calls from GmailWebhookService.ts:41-84
    • Delete import { setupPubSub } and related imports
    • Remove Step 1 ("Set up Pub/Sub") from method
  • Rename setupGmailWebhook()registerWatch() in GmailWebhookService.ts
    • Method now only registers Gmail watch via GmailAdapter.registerWebhookSubscription()
  • Update return type GmailWebhookSetupResult to remove topicPath and subscriptionPath fields
    • Renamed to GmailWatchRegistrationResult and removed topic/subscription fields
  • Update all call sites that reference setupGmailWebhook to use registerWatch

Phase 3: Wire Up New Orchestrator

  • Replace finalizeGoogleProvider calls with configureGmailProvider:
    • In upsertEmailProvider() (emailProviderActions.ts:419-454)
    • In updateEmailProvider() (emailProviderActions.ts:469-507)
  • Remove or reduce finalizeGoogleProvider to thin wrapper (or delete entirely)

Phase 4: UI & Automation Control

  • Add skipAutomation?: boolean = false parameter to:
    • upsertEmailProvider() signature and implementation
    • updateEmailProvider() signature and implementation
    • createEmailProvider() signature and implementation
  • Only call configureGmailProvider() when !skipAutomation
  • Update UI provider form onSubmit() in GmailProviderForm.tsx:91-152:
    • Modify to pass skipAutomation: true for normal saves
    • Keep OAuth flow with skipAutomation: false (default)
  • Add "Refresh Pub/Sub" button that calls configureGmailProvider() with force: true
    • Updated existing "Refresh Watch Subscription" dropdown to use configureGmailProvider(force: true)
    • Modified /api/email/refresh-watch endpoint to call new orchestrator
    • Updated UI text to "Refresh Pub/Sub & Watch" to reflect complete refresh

Current Architecture Analysis

Files Involved:

  • server/src/lib/actions/email-actions/emailProviderActions.ts - Main provider CRUD, contains finalizeGoogleProvider
  • server/src/services/email/GmailWebhookService.ts - Gmail webhook service, currently calls setupPubSub
  • server/src/services/email/EmailProviderService.ts - Contains initializeProviderWebhook
  • server/src/lib/actions/email-actions/setupPubSub.ts - Low-level Pub/Sub setup
  • server/src/components/GmailProviderForm.tsx - UI form for Gmail provider settings

Current Problem Flow:

  1. OAuth: upsertEmailProvider()finalizeGoogleProvider()setupPubSub()
  2. Webhook: finalizeGoogleProvider()EmailProviderService.initializeProviderWebhook()GmailWebhookService.setupGmailWebhook()setupPubSub()
  3. UI Save: updateEmailProvider()finalizeGoogleProvider()setupPubSub()
  4. Nested: Same as step 2 ④

Target Flow:

  • OAuth/Admin: configureGmailProvider()setupPubSub() (once) + registerWatch()
  • UI Save: updateEmailProvider(skipAutomation: true) → (no Pub/Sub calls)

4. Roll-out Strategy

  1. Stage 1 Dual Behaviour
    Backend supports skipAutomation but default is false (old behaviour).
    Ship code; ensures no breakage if UI lags behind.

  2. Stage 2 Frontend Update
    Release UI with skipAutomation: true on normal save and new “Refresh” button.

  3. Stage 3 Final Toggle
    Change backend default of skipAutomation to true and delete old finalizeGoogleProvider dead code.

  4. Monitor logs for unexpected additional Pub/Sub calls; once stable for a week, close ticket.


Outcome Every logical trigger performs exactly one Pub/Sub setup. Google Cloud stays happy; our subscriptions remain active; and the codebase gains a clear separation of concerns that is easy for new contributors to understand.