Governed risk-management platform inside Zoho Projects
Every project manager was supposed to log delivery risks each month. Nobody could tell who actually did, whose projects were flying blind, or what a risk looked like six months ago. The “risk register” was a spreadsheet nobody trusted and no one audited.
How it fits together
No token ever reaches the browser. Every call is brokered server-side by Zoho.
- Zoho Analytics: the authoritative compliance join
- SendGrid + Google Chat: per-owner alerts
The problem
A global software consultancy ran dozens of concurrent client projects, each meant to report delivery risks every month. In practice risks lived as free-text comments buried in tasks. No register, no history, no access control, and no way for leadership to see which active projects were non-compliant or which risks were going stale.
Diagnosis
“Risk tracking” was really two hard problems wearing one hat: a governed data model (risks need immutable history, soft-delete with recovery, and exactly one current version per project and category. Not rows you overwrite), and a compliance engine you can only trust if it joins the register against an authoritative source of truth for which projects are actually active and who owns them.
What I built
- Built the frontend as a vanilla-JS single-page widget embedded in the Zoho Projects right panel. No framework, loaded as dependency-ordered modules with a stale-while-revalidate cache on Zoho app storage.
- Brokered all auth through a Zoho Named Connection: the widget makes zero direct network calls and never holds a token. Zoho injects credentials server-side, and the Cloud Run backend is the only whitelisted egress.
- Backed it with a Flask API on Cloud Run over PostgreSQL (Cloud SQL, IAM auth) with a seven-table schema: the register, an append-only audit timeline, a soft-delete archive, threaded discussions, and three JSONB settings tables.
- Enforced ‘exactly one current risk per project and category’ as a Postgres partial unique index. A database-level invariant, not application hope. While preserving full version history.
- Recorded every change as a field-level JSONB diff (from → to) with actor, IP, and parsed device: compliance-grade provenance for who changed what, from where.
- Computed monthly PM compliance automatically, joining the register against live Zoho Analytics project metadata so cancelled projects never pollute the numbers, and pushed results to each owner via SendGrid email and Google Chat.
- Built a category-locking create/edit/renew wizard that auto-detects intent and hard-aborts on category drift, plus an admin tool that parsed and de-duplicated the old pipe-delimited text risks into the new store.
The hard parts
- Bridged synchronous Flask to the async Cloud SQL IAM connector with one long-lived event loop on a daemon thread. The connector attaches its RSA-key work to whatever loop exists at construction, so a fresh loop per request causes ‘Future attached to a different loop’ crashes.
- Coordinated Zoho OAuth across a fleet of serverless workers with a GCS-backed distributed lock and a fleet-wide cooldown, so independent token refreshes stop tripping Zoho’s per-token throttle.
- Made identity resilient to a company-wide email-domain migration and Unicode name mismatches (NFD normalization), so role and admin lookups don’t silently fail.
Results
Client and internal identifiers changed to preserve confidentiality.