# 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 - [x] 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` - [x] Update `GoogleEmailProviderConfig` interface to include `pubsub_initialised_at?: string` - File: `server/src/components/EmailProviderConfiguration.tsx:73` - [x] 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 - [x] Export `configureGmailProvider` from email-actions index barrel if present (no index barrel exists) ### Phase 2: Trim GmailWebhookService - [x] 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 - [x] Rename `setupGmailWebhook()` → `registerWatch()` in `GmailWebhookService.ts` - Method now only registers Gmail watch via `GmailAdapter.registerWebhookSubscription()` - [x] Update return type `GmailWebhookSetupResult` to remove `topicPath` and `subscriptionPath` fields - Renamed to `GmailWatchRegistrationResult` and removed topic/subscription fields - [x] Update all call sites that reference `setupGmailWebhook` to use `registerWatch` ### Phase 3: Wire Up New Orchestrator - [x] Replace `finalizeGoogleProvider` calls with `configureGmailProvider`: - In `upsertEmailProvider()` (`emailProviderActions.ts:419-454`) - In `updateEmailProvider()` (`emailProviderActions.ts:469-507`) - [x] Remove or reduce `finalizeGoogleProvider` to thin wrapper (or delete entirely) ### Phase 4: UI & Automation Control - [x] Add `skipAutomation?: boolean = false` parameter to: - `upsertEmailProvider()` signature and implementation - `updateEmailProvider()` signature and implementation - `createEmailProvider()` signature and implementation - [x] Only call `configureGmailProvider()` when `!skipAutomation` - [x] 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) - [x] 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.