Context
When a user signs up with email/password, creates events ("Foo"), then connects Google Calendar:
- Expected: Foo persists in Compass, Foo is pushed to Google Calendar, Google events are pulled in
- Actual: Foo disappears — the user sees only their Google Calendar events
Root cause (the disappearing events):
manuallyCreateOrUpdateUser in the SuperTokens middleware looks up the existing user by { "google.googleId": googleUserId }. An email/password user has no googleId yet, so it returns null and a new ObjectId is generated. This new ID becomes the session's userId. googleSignup then calls upsertUserFromAuth({ userId: newId }), which finds no user with that ID and creates a new MongoDB user. The original email/password user (with "Foo") is now a ghost — the session points to a different user entirely.
Secondary goal (mirroring the IndexedDB pattern):
Just as syncLocalEventsToCloud pushes IndexedDB events to Compass during useCompleteAuthentication, we want to push existing COMPASS events to Google Calendar when Google is connected for the first time. Infrastructure already exists: _createGcal, MapEvent.toGcal(), gcalService.createEvent().
Implementation Plan
Step 1 — Fix the root cause: preserve existing user ID
File: packages/backend/src/common/middleware/supertokens.middleware.ts
In manuallyCreateOrUpdateUser, change the MongoDB lookup to include an email fallback:
ts
// Before
{ "google.googleId": input.thirdPartyUserId }
// After
{ $or: [{ "google.googleId": input.thirdPartyUserId }, { email: input.email }] }
This ensures an existing email/password user's _id is reused as the SuperTokens recipe user ID, so the session stays tied to the correct user.
Step 2 — Add CONNECT_GOOGLE auth mode
Files:
packages/backend/src/auth/services/google/google.auth.types.ts — add "CONNECT_GOOGLE" to AuthMode
packages/backend/src/auth/services/google/util/google.auth.util.ts — update determineGoogleAuthMode
determineGoogleAuthMode currently only receives (googleUserId, createdNewRecipeUser). Add email as a third param. When no user is found by googleId, check by email:
ts
export async function determineGoogleAuthMode(
googleUserId: string,
email: string | undefined,
createdNewRecipeUser: boolean,
): Promise<AuthDecision>
Logic addition (after the "not found by googleId" check):
ts
if (!userByGoogleId) {
const userByEmail = email ? await findCompassUserBy("email", email) : null;
if (userByEmail) {
return {
authMode: "CONNECT_GOOGLE",
compassUserId: userByEmail._id.toString(),
...
};
}
return { authMode: "SIGNUP", ... };
}
Update the call site in handleGoogleAuth to pass providerUser.email.
Step 3 — Add connectGoogleToExistingUser handler
File: packages/backend/src/auth/services/google/google.auth.service.ts
Add a new case "CONNECT_GOOGLE" in handleGoogleAuth that calls a new method:
ts
async connectGoogleToExistingUser(
compassUserId: string,
gUser: TokenPayload,
oAuthTokens: Pick<Credentials, "refresh_token" | "access_token">,
)
This method:
- Calls
userService.upsertUserFromAuth({ userId: compassUserId, google: { googleId, picture, gRefreshToken } })
- Sets
sync: { importGCal: "RESTART", incrementalGCalSync: "RESTART" } metadata
- Calls
this.restartGoogleCalendarSyncInBackground(compassUserId) (pull Google → Compass)
- Returns
{ cUserId: compassUserId }
Note: requires a refresh token — enforce with the same guard as SIGNUP mode.
Step 4 — Backend endpoint to push COMPASS events to Google
Files:
packages/backend/src/sync/services/sync.service.ts — new pushCompassEventsToGoogle(userId) method
packages/backend/src/sync/sync.routes.config.ts — new POST /api/sync/push-compass-events route
packages/backend/src/sync/controllers/sync.controller.ts — new controller method
pushCompassEventsToGoogle(userId):
- Fetch all COMPASS-origin events for the user without a
gEventId (these are Compass-native events not yet in Google Calendar)
- Get the gcal client:
getGcalClient(userId) (uses stored refresh token)
- For each event, call
_createGcal(userId, event) — already exists in event.service.ts
- Update each event in MongoDB with the returned
gEventId from Google
- Return
{ pushedCount: number }
Once events have gEventId set, the subsequent full Google Calendar import (triggered by restartGoogleCalendarSync) will match them by gEventId and update in place — no duplicates.
Step 5 — Frontend: mirror the IndexedDB → Compass pattern
Files:
packages/web/src/auth/google/google.auth.util.ts — add pushCompassEvents() function
packages/web/src/common/apis/sync.api.ts — add pushCompassEvents() API call
packages/web/src/auth/hooks/useCompleteAuthentication.ts — call pushCompassEvents() after syncLocalEvents()
Pattern mirrors syncLocalEvents exactly:
ts
// google.auth.util.ts
export async function pushCompassEvents(): Promise<{ pushedCount: number; success: boolean; error?: Error }> {
try {
const result = await SyncApi.pushCompassEvents();
return { pushedCount: result.pushedCount, success: true };
} catch (error) {
return { pushedCount: 0, success: false, error: error as Error };
}
}
In useCompleteAuthentication, after syncLocalEvents():
ts
const pushResult = await pushCompassEvents();
if (!pushResult.success) {
// Non-critical — events still exist in Compass, just not in Google Calendar yet
console.error(pushResult.error);
}
// triggerFetch already dispatched — will show updated events with gEventId
No toast needed on failure (events are preserved in Compass; pushing to Google Calendar can be silent).
Critical Files
File | Change
-- | --
packages/backend/src/common/middleware/supertokens.middleware.ts | Email fallback in manuallyCreateOrUpdateUser MongoDB query
packages/backend/src/auth/services/google/google.auth.types.ts | Add "CONNECT_GOOGLE" to AuthMode
packages/backend/src/auth/services/google/util/google.auth.util.ts | Add email param + CONNECT_GOOGLE detection in determineGoogleAuthMode
packages/backend/src/auth/services/google/google.auth.service.ts | Add connectGoogleToExistingUser + CONNECT_GOOGLE case in handleGoogleAuth
packages/backend/src/sync/services/sync.service.ts | New pushCompassEventsToGoogle(userId) method
packages/backend/src/sync/sync.routes.config.ts | New POST /push-compass-events route
packages/backend/src/sync/controllers/sync.controller.ts | New controller handler
packages/web/src/auth/google/google.auth.util.ts | Add pushCompassEvents() wrapper
packages/web/src/common/apis/sync.api.ts | Add pushCompassEvents() API call
packages/web/src/auth/hooks/useCompleteAuthentication.ts | Call pushCompassEvents() after syncLocalEvents()
Reusable Infrastructure (do not rewrite)
_createGcal(userId, event) — packages/backend/src/event/services/event.service.ts
MapEvent.toGcal(event) — packages/core/src/mappers/map.event.ts
getGcalClient(userId) — packages/backend/src/auth/services/google/clients/google.calendar.client.ts
findCompassUserBy(key, value) — packages/backend/src/user/queries/user.queries.ts
Verification
- Root cause fix: Sign up with email, create event "Foo", connect Google → "Foo" should still appear in Compass after connecting
- Push to Google: After connecting, open Google Calendar → "Foo" should appear as a new event
- No duplicates: "Foo" should appear exactly once in Compass after the full Google Calendar import runs
- Existing Google users unaffected: Sign up with Google → sign in again →
SIGNIN_INCREMENTAL mode still works normally
- Reconnect still works: A Google user whose token expired goes through
RECONNECT_REPAIR, not CONNECT_GOOGLE
- New Google signups unaffected: Brand new user signing up with Google →
SIGNUP mode, no COMPASS events to push (no-op)
- Tests: Update
google.auth.service.test.ts, google.auth.util.ts tests for new mode; add test for connectGoogleToExistingUser
Context
When a user signs up with email/password, creates events ("Foo"), then connects Google Calendar:
Root cause (the disappearing events):
manuallyCreateOrUpdateUserin the SuperTokens middleware looks up the existing user by{ "google.googleId": googleUserId }. An email/password user has nogoogleIdyet, so it returnsnulland a new ObjectId is generated. This new ID becomes the session'suserId.googleSignupthen callsupsertUserFromAuth({ userId: newId }), which finds no user with that ID and creates a new MongoDB user. The original email/password user (with "Foo") is now a ghost — the session points to a different user entirely.Secondary goal (mirroring the IndexedDB pattern): Just as
syncLocalEventsToCloudpushes IndexedDB events to Compass duringuseCompleteAuthentication, we want to push existing COMPASS events to Google Calendar when Google is connected for the first time. Infrastructure already exists:_createGcal,MapEvent.toGcal(),gcalService.createEvent().Implementation Plan
Step 1 — Fix the root cause: preserve existing user ID
File:
packages/backend/src/common/middleware/supertokens.middleware.tsIn
manuallyCreateOrUpdateUser, change the MongoDB lookup to include an email fallback:This ensures an existing email/password user's
_idis reused as the SuperTokens recipe user ID, so the session stays tied to the correct user.Step 2 — Add
CONNECT_GOOGLEauth modeFiles:
packages/backend/src/auth/services/google/google.auth.types.ts— add"CONNECT_GOOGLE"toAuthModepackages/backend/src/auth/services/google/util/google.auth.util.ts— updatedetermineGoogleAuthModedetermineGoogleAuthModecurrently only receives(googleUserId, createdNewRecipeUser). Addemailas a third param. When no user is found bygoogleId, check byemail:Logic addition (after the "not found by googleId" check):
Update the call site in
handleGoogleAuthto passproviderUser.email.Step 3 — Add
connectGoogleToExistingUserhandlerFile:
packages/backend/src/auth/services/google/google.auth.service.tsAdd a new
case "CONNECT_GOOGLE"inhandleGoogleAuththat calls a new method:This method:
userService.upsertUserFromAuth({ userId: compassUserId, google: { googleId, picture, gRefreshToken } })sync: { importGCal: "RESTART", incrementalGCalSync: "RESTART" }metadatathis.restartGoogleCalendarSyncInBackground(compassUserId)(pull Google → Compass){ cUserId: compassUserId }Note: requires a refresh token — enforce with the same guard as SIGNUP mode.
Step 4 — Backend endpoint to push COMPASS events to Google
Files:
packages/backend/src/sync/services/sync.service.ts— newpushCompassEventsToGoogle(userId)methodpackages/backend/src/sync/sync.routes.config.ts— newPOST /api/sync/push-compass-eventsroutepackages/backend/src/sync/controllers/sync.controller.ts— new controller methodpushCompassEventsToGoogle(userId):gEventId(these are Compass-native events not yet in Google Calendar)getGcalClient(userId)(uses stored refresh token)_createGcal(userId, event)— already exists inevent.service.tsgEventIdfrom Google{ pushedCount: number }Once events have
gEventIdset, the subsequent full Google Calendar import (triggered byrestartGoogleCalendarSync) will match them bygEventIdand update in place — no duplicates.Step 5 — Frontend: mirror the IndexedDB → Compass pattern
Files:
packages/web/src/auth/google/google.auth.util.ts— addpushCompassEvents()functionpackages/web/src/common/apis/sync.api.ts— addpushCompassEvents()API callpackages/web/src/auth/hooks/useCompleteAuthentication.ts— callpushCompassEvents()aftersyncLocalEvents()Pattern mirrors
syncLocalEventsexactly:In
useCompleteAuthentication, aftersyncLocalEvents():No toast needed on failure (events are preserved in Compass; pushing to Google Calendar can be silent).
Critical Files
Reusable Infrastructure (do not rewrite)
_createGcal(userId, event)—packages/backend/src/event/services/event.service.tsMapEvent.toGcal(event)—packages/core/src/mappers/map.event.tsgetGcalClient(userId)—packages/backend/src/auth/services/google/clients/google.calendar.client.tsfindCompassUserBy(key, value)—packages/backend/src/user/queries/user.queries.tsVerification
SIGNIN_INCREMENTALmode still works normallyRECONNECT_REPAIR, notCONNECT_GOOGLESIGNUPmode, no COMPASS events to push (no-op)google.auth.service.test.ts,google.auth.util.tstests for new mode; add test forconnectGoogleToExistingUser