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.
This commit is contained in:
2026-08-25 14:45:18 -05:00
parent b38c46aaec
commit 777efd168d
11 changed files with 595 additions and 29 deletions
+166
View File
@@ -0,0 +1,166 @@
# 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:**
```bash
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.