Files
rootnode-academy/docs/RUNBOOK_GOOGLE_WORKSPACE_EMAIL.md
T
ced4568 777efd168d docs(phase5c): enrollment communications documentation + Google Workspace runbook
- PHASE5C_ENROLLMENT_COMMUNICATIONS.md: the full design doc.
- RUNBOOK_GOOGLE_WORKSPACE_EMAIL.md: every REQUIRES GOOGLE WORKSPACE ADMIN ACTION step
  (mailbox, service account, domain-wide delegation scoped to gmail.send only, SPF/DKIM/DMARC),
  environment variables, testing procedure, production secret handling, rotation, revocation,
  troubleshooting, quotas.
- RUNBOOK_STUDENT_ACTIVATION.md: day-to-day operation of the pipeline from /admin.
- WEBSITE_ENROLLMENT_INTEGRATION.md: explicit website-vs-RootNode responsibility boundary,
  the new contact_email field, and closes the 'RootNode doesn't send activation email yet'
  known limitation this doc previously documented.
- RUNBOOK_SELF_PACED_ENROLLMENT.md, RUNBOOK_STUDENT_ACCOUNT_LIFECYCLE.md: point at the new
  automatic-email behavior and the Resend activation control.
- REMOTE_AND_SELF_PACED_ARCHITECTURE.md, ENGINEERING_SOP.md: close the enrollment-
  communication gap; new ENGINEERING_SOP §16.
- PRODUCTION_VALIDATION_CHECKLIST.md, README.md, platform/README.md: cross-references and a
  Phase 5C production-validation section.
2026-08-25 14:45:18 -05:00

10 KiB

Runbook: Google Workspace Email Configuration

Everything in this document happens OUTSIDE this repository, in the Google Admin Console and Google Cloud Console, and is marked REQUIRES GOOGLE WORKSPACE ADMIN ACTION. Nothing here can be scripted or automated from the codebase — Chase (or whoever holds Workspace super-admin) has to do these steps by hand, once, before RootNode Academy can send real email.

See docs/PHASE5C_ENROLLMENT_COMMUNICATIONS.md §2 for why this mechanism was chosen over SMTP or a human login.

1. Selected mechanism

Gmail API (users.messages.send) via a Google Cloud service account using Workspace domain-wide delegation, impersonating one dedicated sending mailbox. No human password is ever stored by RootNode. The delegated scope is exactly https://www.googleapis.com/auth/ gmail.send — nothing broader.

2. REQUIRES GOOGLE WORKSPACE ADMIN ACTION: create the sending mailbox

Decide the address RootNode sends as — e.g. [email protected] or [email protected]. This repository does not assume or hard-code one; it's whatever you set EMAIL_FROM_ADDRESS to (§5).

  1. In Google Admin Console (admin.google.com) → Directory → Users, create this mailbox as a real Workspace user (or a Google Group with "Collaborative Inbox" if you'd rather route replies to a shared team inbox — either works with this mechanism, but a Group needs its own delegation setup; a plain user mailbox is simpler and is what this runbook assumes).
  2. Consider giving it a strong, unique, never-reused password and enabling 2-Step Verification on it anyway, even though RootNode's application will never use that password — this account still exists as a real Workspace identity and should be secured like any other.

3. REQUIRES GOOGLE WORKSPACE ADMIN ACTION: create the service account (Google Cloud Console)

  1. In Google Cloud Console (console.cloud.google.com), create or choose a GCP project associated with your Workspace organization.
  2. Enable the Gmail API for that project (APIs & Services → Library → search "Gmail API" → Enable).
  3. IAM & Admin → Service Accounts → Create Service Account. Name it something identifiable (e.g. rootnode-academy-mailer). No project-level IAM roles are needed for this — it only ever calls the Gmail API as the impersonated user, not as itself.
  4. On that service account, create a JSON key (Keys → Add Key → Create new key → JSON). This downloads a file containing client_email and private_key — this is the sensitive credential, treat it exactly like a password (see §8).
  5. Copy the service account's Client ID (a long numeric string, on the service account's detail page) — needed for the next step.

4. REQUIRES GOOGLE WORKSPACE ADMIN ACTION: authorize domain-wide delegation

  1. Google Admin Console → Security → Access and data control → API controls → Domain-wide Delegation → Add new.
  2. Client ID: the numeric ID from step 3.5 above.
  3. OAuth Scopes: https://www.googleapis.com/auth/gmail.send — only this scope. Do not add broader Gmail scopes (readonly, modify, etc.) — RootNode's adapter never needs them and granting them would violate least-privilege for no benefit.
  4. Authorize.

This is the step that lets the service account act as EMAIL_FROM_ADDRESS specifically — it does not grant access to any other mailbox in the domain.

5. RootNode-side environment variables

Set these on the Platform API's environment (production: real secret storage — see §8; local dev: platform/.env, already gitignored):

Variable Value Notes
EMAIL_PROVIDER gws Unset/anything else defaults to the dev provider — never real mail.
GOOGLE_SERVICE_ACCOUNT_JSON the full JSON key file's contents, as one string The sensitive credential — see §8.
EMAIL_FROM_ADDRESS the mailbox from §2, e.g. [email protected] Also the delegated-user (sub) claim.
EMAIL_FROM_NAME e.g. RootNode Academy Display name only, not sensitive.
EMAIL_REPLY_TO optional, e.g. [email protected] Where a student's reply actually goes, if different from the sending address.
PUBLIC_APP_ORIGIN e.g. https://learn.rootnodeacademy.com The origin activation/reset links are built from (§7 of the Phase 5C authorization) — required for any token-bearing email to render a working link.

6. DNS: SPF, DKIM, DMARC (REQUIRES DNS ADMIN ACTION — not performed from this repository)

Google Workspace's own documentation is authoritative here; this is a summary of what must exist before mail sent via the Gmail API from a Workspace mailbox reliably lands in inboxes instead of spam:

  • SPF: your domain's TXT record must include Google's SPF include (include:_spf.google.com) if Workspace is your MX provider — check Admin Console → Apps → Google Workspace → Gmail → Authenticate email for the exact current record Google recommends for your domain.
  • DKIM: Admin Console → Apps → Google Workspace → Gmail → Authenticate email → generate a DKIM key for the domain, then publish the TXT record it gives you, then return to that page and turn DKIM signing ON. Gmail API sends from a Workspace mailbox are signed the same way regular Gmail sends are once this is on.
  • DMARC: publish a _dmarc TXT record (start with p=none to monitor before enforcing) once SPF and DKIM both pass consistently.

Do not change DNS from this repository or these instructions — this is exactly the kind of external, admin-console/DNS-provider action that stays outside the codebase. Verify current exact record values in Google's own Workspace admin help before publishing anything.

7. Testing procedure

Local/dev — no live credentials needed at all. EMAIL_PROVIDER unset (or anything other than gws) uses the deterministic dev provider (email/dev-provider.js) — every notification still goes through the full queue/worker/retry pipeline, but nothing ever reaches a real network. platform/test/e2e-phase5c.js and the Phase 5C drills run entirely this way.

Live delivery — opt-in, one message, once credentials exist:

RUN_GWS_EMAIL_TEST=true \
TEST_EMAIL_RECIPIENT=[email protected] \
GOOGLE_SERVICE_ACCOUNT_JSON="$(cat /path/to/service-account-key.json)" \
EMAIL_FROM_ADDRESS=[email protected] \
EMAIL_FROM_NAME="RootNode Academy" \
node infra/scripts/gws-email-test.js

Sends exactly one clearly-marked test message to TEST_EMAIL_RECIPIENT — never inferred from production/student data, never run automatically as part of any regression suite. Without RUN_GWS_EMAIL_TEST=true it does nothing and exits cleanly.

8. Production secret handling

  • GOOGLE_SERVICE_ACCOUNT_JSON is exactly as sensitive as a password — store it in whatever secret manager/environment-injection mechanism the production deployment uses (see docs/PRODUCTION_DEPLOYMENT.md for the general pattern this repo follows for other secrets like PROVISIONER_SHARED_SECRET). Never commit it, never put it in a Docker image layer, never log it. gws-provider.js reads it fresh from the environment on every send and never writes it to disk.
  • Nothing in this codebase ever returns GOOGLE_SERVICE_ACCOUNT_JSON's contents, an access token, or any part of it through an API response, log line, or audit event.

9. Credential rotation

  1. Google Cloud Console → the service account → Keys → Add Key → Create new key (JSON) — this creates an ADDITIONAL valid key without invalidating the old one yet.
  2. Update GOOGLE_SERVICE_ACCOUNT_JSON in production's secret store to the new key, redeploy/ restart the Platform API.
  3. Confirm delivery still works (§7's live test, or just watch /api/admin/notifications for a real send after the restart).
  4. Google Cloud Console → the service account → Keys → delete the OLD key.

Domain-wide delegation authorization (§4) is tied to the service account's Client ID, not to any individual key — rotating keys does not require re-authorizing delegation.

10. Revocation (compromised credential / offboarding)

  1. Google Cloud Console → the service account → Keys → delete the affected key immediately — this invalidates it instantly, before anything else.
  2. If the whole service account should stop being able to send at all: Google Admin Console → Security → API controls → Domain-wide Delegation → remove that Client ID's authorization.
  3. Rotate EMAIL_FROM_ADDRESS's own Workspace login password as a precaution even though the application never used it — a compromised service-account key doesn't imply the mailbox's own credentials were exposed, but treat it as a "review this account" trigger.

11. Troubleshooting

Symptom Likely cause
Every send fails with gws_not_configured GOOGLE_SERVICE_ACCOUNT_JSON or EMAIL_FROM_ADDRESS unset/malformed in the Platform API's environment.
gws_auth_failed Domain-wide delegation not authorized for this Client ID/scope, or the service account key was deleted/revoked. Re-check §4.
gws_http_403 The delegated sub (EMAIL_FROM_ADDRESS) isn't a real mailbox in this Workspace domain, or the Gmail API isn't enabled on the GCP project (§3.2).
gws_rate_limited (HTTP 429) Gmail API per-user sending limits hit — see §12. The notification worker retries this automatically with backoff.
Mail sends successfully but lands in spam SPF/DKIM/DMARC (§6) not fully configured/passing yet — this is a deliverability issue, not an application error.

12. Google Workspace quotas/limits relevant to this mechanism

Gmail API sending inherits the same per-user daily sending limits as regular Gmail/Workspace sending (limits vary by Workspace edition — check current figures in Google's own Workspace admin documentation, as these change over time and are not something to hard-code here). For RootNode Academy's transactional volume (activation/enrollment/reset emails, not bulk/marketing — explicitly out of scope, see the Phase 5C authorization §5/§28), this should sit comfortably under any Workspace edition's per-user limit; revisit if enrollment volume grows substantially.