Workspace Gmail

    Connects a Google account per workspace so automated emails (welcome emails, brochures) are sent from the org's own Gmail address.

    • API: apps/api/src/gmail/ (prefix /v1/workspace/gmail)
    • SDK: @branofy/api/gmail
    • UI: apps/premium/app/(dashboard)/settings/integrations/email/page.tsx
    • Schema: gmail_connections (one row per workspace)

    See README.md for the shared OAuth model, signed state, and env vars.

    Endpoints

    | Method | Path | Auth | Purpose | | --- | --- | --- | --- | | GET | /v1/workspace/gmail/auth | session/api‑key | Returns { url } — the Google consent URL (access_type=offline, prompt=consent). | | GET | /v1/workspace/gmail/callback | public | Exchanges code → tokens + email → stores connection → 302 to settings. | | GET | /v1/workspace/gmail/status | session/api‑key | { connected, email }. | | DELETE | /v1/workspace/gmail/status | session/api‑key | Disconnect (deletes the row). |

    Connect flow

    sequenceDiagram
        actor U as User (browser)
        participant A as NestJS API (/v1/workspace/gmail)
        participant G as Google OAuth
        participant I as googleapis.com
    
        U->>A: GET /v1/workspace/gmail/auth → { url }
        U->>G: redirect to consent (access_type=offline, prompt=consent)
        G-->>A: GET /v1/workspace/gmail/callback ?code & state
        A->>A: verify state
        A->>I: POST oauth2/token → access + refresh tokens
        A->>I: GET oauth2/v2/userinfo → email
        A->>A: upsert gmail_connections (by organization_id)
        A-->>U: 302 → /settings/integrations/email?gmail=connected&address=...
    
    1. UI fetches /api/workspace/gmail/auth, redirects to Google's consent screen.
    2. Google redirects to {API_PUBLIC_URL}/v1/workspace/gmail/callback?code&state.
    3. Callback exchanges the code at oauth2.googleapis.com/token, reads the address from oauth2/v2/userinfo, and upserts gmail_connections for the org.
    4. Redirect → {APP}/settings/integrations/email?gmail=connected&address=....

    OAuth scopes

    https://www.googleapis.com/auth/gmail.send, https://www.googleapis.com/auth/userinfo.email, openid.

    Credentials

    Google OAuth client is env‑only: GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET. Authorized redirect URI: {API_PUBLIC_URL}/v1/workspace/gmail/callback. The Gmail API must be enabled on the Google Cloud project.

    Reconnecting

    The connection is keyed by organization_id (one Gmail per workspace). Reconnecting updates the existing row with a fresh access token, expiry, scope, and email.

    flowchart TD
        R[connect runs again] --> C{row exists for org?}
        C -- no --> I[INSERT gmail_connections]
        C -- yes --> U[UPDATE access token, expiry, scope, email]
        U --> Q{response has refresh_token?}
        Q -- yes --> N[store new refresh token]
        Q -- no --> K[KEEP existing refresh token]
        I --> D[done]
        N --> D
        K --> D
    

    Refresh‑token preservation: Google only returns a refresh_token on the first consent (or with prompt=consent). The callback keeps the previously stored refresh token whenever the new token response omits one, so a reconnect never wipes the ability to refresh access tokens offline.

    Access tokens are short‑lived (~1h) and meant to be refreshed using the stored refresh token by the email‑sending worker. Disconnecting deletes the row; a later connect is a clean first‑time consent.

    Troubleshooting

    | Symptom | Cause / fix | | --- | --- | | gmail=error&reason=... | Google denied or misconfigured — check the authorized redirect URI and that the Gmail API is enabled. | | 400 from /auth | GOOGLE_CLIENT_ID not set. | | No refresh token after reconnect | Expected — the original is preserved; force a new one by disconnecting first, then reconnecting. |