Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
7.1 KiB
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
- Background & Current Problem
- Target Architecture (diagram & responsibilities)
- Phased To-Do List
- Acceptance Criteria / Testing
- 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 TIMESTAMPTZfield togoogle_email_provider_configtable- File:
server/migrations/20250719133450_add_pubsub_initialised_at_to_google_config.cjs
- File:
- Update
GoogleEmailProviderConfiginterface to includepubsub_initialised_at?: string- File:
server/src/components/EmailProviderConfiguration.tsx:73
- File:
- Create
server/src/lib/actions/email-actions/configureGmailProvider.ts- Copy logic from
finalizeGoogleProvider(emailProviderActions.ts:280-337) - Add
force?: booleanparameter 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()(notsetupGmailWebhook) - Update
pubsub_initialised_at = NOW()after successful Pub/Sub setup
- Copy logic from
- Export
configureGmailProviderfrom 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
- Delete
- Rename
setupGmailWebhook()→registerWatch()inGmailWebhookService.ts- Method now only registers Gmail watch via
GmailAdapter.registerWebhookSubscription()
- Method now only registers Gmail watch via
- Update return type
GmailWebhookSetupResultto removetopicPathandsubscriptionPathfields- Renamed to
GmailWatchRegistrationResultand removed topic/subscription fields
- Renamed to
- Update all call sites that reference
setupGmailWebhookto useregisterWatch
Phase 3: Wire Up New Orchestrator
- Replace
finalizeGoogleProvidercalls withconfigureGmailProvider:- In
upsertEmailProvider()(emailProviderActions.ts:419-454) - In
updateEmailProvider()(emailProviderActions.ts:469-507)
- In
- Remove or reduce
finalizeGoogleProviderto thin wrapper (or delete entirely)
Phase 4: UI & Automation Control
- Add
skipAutomation?: boolean = falseparameter to:upsertEmailProvider()signature and implementationupdateEmailProvider()signature and implementationcreateEmailProvider()signature and implementation
- Only call
configureGmailProvider()when!skipAutomation - Update UI provider form
onSubmit()inGmailProviderForm.tsx:91-152:- Modify to pass
skipAutomation: truefor normal saves - Keep OAuth flow with
skipAutomation: false(default)
- Modify to pass
- Add "Refresh Pub/Sub" button that calls
configureGmailProvider()withforce: true- Updated existing "Refresh Watch Subscription" dropdown to use
configureGmailProvider(force: true) - Modified
/api/email/refresh-watchendpoint to call new orchestrator - Updated UI text to "Refresh Pub/Sub & Watch" to reflect complete refresh
- Updated existing "Refresh Watch Subscription" dropdown to use
Current Architecture Analysis
Files Involved:
server/src/lib/actions/email-actions/emailProviderActions.ts- Main provider CRUD, containsfinalizeGoogleProviderserver/src/services/email/GmailWebhookService.ts- Gmail webhook service, currently callssetupPubSubserver/src/services/email/EmailProviderService.ts- ContainsinitializeProviderWebhookserver/src/lib/actions/email-actions/setupPubSub.ts- Low-level Pub/Sub setupserver/src/components/GmailProviderForm.tsx- UI form for Gmail provider settings
Current Problem Flow:
- OAuth:
upsertEmailProvider()→finalizeGoogleProvider()→setupPubSub()① - Webhook:
finalizeGoogleProvider()→EmailProviderService.initializeProviderWebhook()→GmailWebhookService.setupGmailWebhook()→setupPubSub()② - UI Save:
updateEmailProvider()→finalizeGoogleProvider()→setupPubSub()③ - 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
-
Stage 1 – Dual Behaviour
Backend supportsskipAutomationbut default isfalse(old behaviour).
Ship code; ensures no breakage if UI lags behind. -
Stage 2 – Frontend Update
Release UI withskipAutomation: trueon normal save and new “Refresh” button. -
Stage 3 – Final Toggle
Change backend default ofskipAutomationtotrueand delete oldfinalizeGoogleProviderdead code. -
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.