← All decisions

Google Calendar integration: trust model

accepted

0022 — Google Calendar integration: trust model

  • Status: accepted
  • Date: 2026-05-15
  • Deciders: Derek

Context

Per ADR 0010, ark surfaces Google Workspace data; Google holds it. The Calendar v1 workstream is the first concrete integration built on that stance. This ADR documents exactly what ark asks of a tenant who connects Google Calendar, and what ark cannot do regardless of consent — for tenant admins evaluating whether to connect.

Decision

When an admin connects Google Calendar to an ark organization, ark requests two Google scopes:

  • calendar.readonly — read the events on calendars the admin has access to
  • calendar.calendarlist.readonly — list the calendars on the admin’s account so the admin can pick which one to share with the org

These are Google’s “sensitive” tier — narrower than full Calendar (which would include write access ark does not need).

One admin per org. Connecting is an admin action. Members of the org never sign in to Google through ark. The portal reads events server-side using the admin’s stored credentials and renders them to every member who visits the calendar page.

Refresh tokens live in Supabase Vault. When the admin grants access, ark exchanges Google’s authorization code for a refresh token and stores that token in Supabase Vault — Supabase’s purpose-built encrypted-at-rest secret store, separated from the application database. The google_oauth_credentials table records who connected and when, but it stores only an opaque UUID pointing into Vault — never the token itself.

Single calendar per org. The admin picks one calendar to share. Members see exactly that calendar. The admin can change the selection or disconnect at any time.

What ark cannot do, by design, regardless of consent:

  • Read or send email (Gmail). Not requested. Cannot be requested without amending this ADR and re-consenting every tenant.
  • Read or modify Drive files (Docs, Sheets, …). Out of scope for Calendar v1. Each future scope is added with a separate ADR.
  • Write to the calendar — create events, delete events, change attendees. We requested read-only scopes; Google will reject writes.
  • Read calendars other than the one the admin selected. (We can technically list them — that’s what calendarlist.readonly is for — but we read events from exactly one.)

Disconnect deletes the credential row. When the admin clicks Disconnect, ark revokes the refresh token at Google, deletes the Vault secret, deletes the credential row, deletes the selected-calendar row, deletes the event cache. Reconnect starts from scratch.

If Google revokes our access (e.g., the admin changed their Google password, Google’s security policies flagged the app, the admin manually revoked at myaccount.google.com/permissions), ark detects this on the next API call, marks the credential revoked, and shows a “reconnect” banner to the admin. Members see a “calendar unavailable” message until reconnect.

Transparency. Every Google API call ark makes on the org’s behalf is recorded in google_audit_log (a separate table from the cross-tenant org_access_audit). The admin can view it at /admin/google/audit — what action, when, what came back, did it succeed. This matches the ADR 0008 “tenants can see what ark does on their behalf” promise.

Consequences

Easier:

  • The trust ask is minimal. A skeptical tenant can read this ADR, see the audit log, and inspect Google’s “third-party app access” page to confirm only the named scopes were granted.
  • Members never enter the Google trust loop. Their personal Google accounts are unaffected.
  • Disconnect is genuinely clean — Vault deletion, Google revocation, all state removed.

Harder:

  • Only the admin’s account can grant access. If the admin changes (e.g., new ED), reconnection is required. Documented in the admin onboarding flow.
  • We depend on Google’s OAuth verification process to scale. While in Google’s “Testing” mode, the app supports up to 100 connected admin users total across all ark tenants. Before scaling beyond that, ark goes through Google’s Cloud Application Security Assessment (CASA) review for sensitive scopes — an operational item, not a code change.
  • Tenants without Google Workspace (or who use personal Google accounts) work the same way, mechanically. The “admin’s personal Google account” path is supported because OAuth doesn’t distinguish Workspace from personal — only Google does, and only at the consent screen.

Revisit if:

  • A new scope is needed for an ark feature (each addition is its own ADR amendment).
  • Google changes the OAuth model in a way that affects this trust posture.
  • We need to support a tenant where the admin and the calendar-owner are different people.

Alternatives considered

  • Per-member Google sign-in. Every member OAuths individually so the calendar query runs as their identity. Rejected: forces every member to connect a Google account, dramatically more friction, and the calendar we surface is org-shared, not personal.
  • Service account. A Google-issued service identity ark uses to read tenant calendars. Rejected for personal Google accounts (service accounts only work in Workspace), creating a two-track integration. Single-admin OAuth covers both.
  • Wider scope (calendar full). Would let ark create events from the portal. Rejected for Calendar v1 — broader scope = harder trust ask, and we have no concrete write feature yet.
  • No Vault, encrypted column. Equivalent security if done well, but DIY. Vault is purpose-built; one fewer cryptographic primitive to own.

Operational prerequisites (one-time)

Before the OAuth flow goes live (PR 2 in the rollout sequence), the operator runs pnpm google:setup — a guided wizard that:

  • Creates the GCP project and enables the Calendar API (automated via gcloud).
  • Opens the Cloud Console at the right URL for the consent screen and the OAuth Client ID (Console-only — Google does not expose these surfaces to gcloud for user-facing web OAuth in May 2026), prints the exact scope strings + redirect URIs to paste in, and accepts the resulting client_id / client_secret back.
  • Writes GOOGLE_OAUTH_CLIENT_ID, GOOGLE_OAUTH_CLIENT_SECRET, GOOGLE_OAUTH_REDIRECT_URI to local .env.

The same three env vars must be set on Railway (ark-api service) for production. If they are missing at deploy time, the API refuses to start — by design, fail-fast rather than fail-mid-OAuth.

Full runbook (including OAuth verification, billing, troubleshooting): docs/SETUP-GOOGLE-CLOUD.md.