Google Cloud
Cloudflare
Articulate 360
Rise 360
Moodle
---
name: AI101L Prompt Cell POC — End-to-End Architecture
description: Verbose architecture doc for the weekend POC. Adversarial review of every stack choice + locked decisions + Mermaid diagrams with Google Cloud stencils + PIPEDA notes + cost model + 3-day build plan.
type: architecture
version: 1.2
status: LIVE — POC deployed 2026-08-24 on Cloudflare edge + Vertex Gemini 2.5 Flash Montreal
authored: 2026-08-22
last_updated: 2026-08-24
author: MCCC-CITT
owner: mohammed@adbalabs.com
supersedes:
- v1.0 (2026-08-22 initial proposal)
- v1.1 (2026-08-23 founder-approved Toronto-region variant, superseded because 2.5 Flash is not GA in northamerica-northeast2)
changelog:
- "v1.2 (2026-08-24): Region shift Toronto → Montreal (northamerica-northeast2 → northamerica-northeast1). Both are Canadian, PIPEDA-safe. Added SA-key POC auth path alongside WIF. Live URLs added."
- "v1.1 (2026-08-23): Founder-approved v1 draft after Q&A round."
- "v1.0 (2026-08-22): Initial architecture proposal."
related_tickets:
- mc#1774 (MASTER EPIC ADBA-AI-LABS)
- mc#1779 (M1 Rise 360 Embed Block insertion — parked pending Articulate build)
- mc#1629 (W5 Moodle deployment)
- mc#1419 (GL0 pre-flight)
- PR #1775 (POC scaffold + this doc)
live_urls:
frontend: https://ai101l-lab.pages.dev/
worker_api: https://ai101l-worker.ai101l-worker.workers.dev/api/v1/*
architecture_doc: https://ai101l-arch.pages.dev/
---
# AI101L Prompt Cell POC — End-to-End Architecture
> **🟢 STATUS: LIVE.** POC deployed 2026-08-24 on Cloudflare edge + real Vertex Gemini 2.5 Flash (Montreal). URLs:
> - Frontend: `https://ai101l-lab.pages.dev/`
> - Worker API: `https://ai101l-worker.ai101l-worker.workers.dev/api/v1/*`
> - Architecture doc (this doc, published): `https://ai101l-arch.pages.dev/`
>
> **Purpose.** The full POC stack, front-to-back, with adversarial justification for every choice. Originally written before build to gate founder approval; now updated to reflect the built + deployed reality (v1.2, 2026-08-24).
>
> **Scope.** L1 lab is the anchor for the iframe-embed proof. All 7 lab configs (L1–L7) are wired and callable via `labId` in the JWT — the Worker routes every one against the same Vertex endpoint.
>
> **Non-scope for POC.** Multi-model routing, adaptive rate limits, admin dashboard, WCAG audit, all seven SCORM packages, CITT Moodle deployment. Those are Week 2–5.
---
## 1. Adversarial review — industry best practice vs each locked choice
Every decision on the stack is stated, then adversarially compared against what a Sept-2026 industry peer (a well-run EdTech / logistics-training startup shipping AI labs) would pick. Verdict is either **KEEP** (choice survives review) or **CHANGE** (better option exists) or **NOTE** (choice is fine but with caveat).
### 1.1 Model — Gemini 2.5 Flash (northamerica-northeast1 · Montreal)
| Alternative | Pros | Cons | Verdict vs our pick |
|---|---|---|---|
| **Gemini 2.5 Flash** (locked, in Montreal) | Cheapest per token in class; 1M context; Vertex-native = one credential stack for auth; Canadian region hits PIPEDA cleanly | Model drift risk (Vertex GA releases can shift behaviour); Vertex quotas require quota-increase asks; ⚠ NOT available in Toronto (northeast2) — see §1.9 for the region-shift decision | — |
| GPT-4o-mini via OpenAI API | Best latency in North America; broadest ecosystem; strong tool-use | US-only data path (PIPEDA disclosure required); adds a second billing stack and a second SDK | Our pick wins on residency + billing consolidation |
| Claude Haiku 4.5 via Bedrock (`ca-central-1`) | Also Canadian region; strong reasoning per $; excellent instruction-following | Bedrock adds AWS billing + SigV4 signing complexity from CF Workers | Our pick wins on simpler auth |
| Llama 3.3 70B via Groq | Fastest ($/token) tokens/sec | US-only; open-weight fine but no SLA; less compliance story for a CITT partnership | Not appropriate for regulated-partner POC |
| Vertex Gemini 2.5 Pro | Higher quality on hard reasoning | 10–15× cost of Flash; capstone-only use case | Reserve for M7 in Week 3+, not POC |
**Industry BP (2026):** Multi-provider abstraction is the accepted architecture — but you don't build it in a weekend POC. Ship one model, wrap it behind a clean interface, add providers when you know which failure mode you're solving.
**Verdict — KEEP.** Gemini 2.5 Flash in `northamerica-northeast1` (Montreal — see §1.9). Wrap the Vertex client behind a `LLMClient` interface so switching providers later is a one-file change. **VERIFIED LIVE 2026-08-24:** L1 prompt returns 471 tokens / 8.2s / `finishReason=STOP` end-to-end via edge-deployed Worker.
### 1.2 Hosting — Cloudflare Pages + Cloudflare Workers
| Alternative | Pros | Cons | Verdict |
|---|---|---|---|
| **CF Pages + Workers** (locked) | Free-tier: 100k rq/day + unlimited static bandwidth; sub-50ms cold start; global edge; built-in KV + D1; native custom-domain SSL | Data plane in ~300 PoPs (some outside Canada) — but processing is stateless; telemetry can be Canada-only | — |
| Vercel + Vercel Functions | Best DX; excellent Next.js support | 100k function invocations/mo free tier is lower; higher $/rq at scale; edge functions in beta | CF wins on cost + edge-first |
| Netlify + Netlify Functions | Solid CDN; simple builds | Function limits tighter; less mature edge story | CF wins on quotas |
| GCP Cloud Run + GCS static | All-Google (Toronto region); cleanest residency story | Cold starts 500ms–2s; requires container build; $5–20/mo baseline even at zero traffic; no edge caching without CDN in front | Overkill for POC; revisit if Cloudflare edge-PoP residency becomes a legal issue |
| Fly.io Machines | Nice deploy story; Toronto region | Higher fixed cost; smaller edge presence | Worse than CF on cost + edge |
| AWS CloudFront + Lambda@Edge | Widely used; strong tooling | Lambda@Edge quota limits (128MB, no Node fetch to arbitrary hosts easily); billing complexity | Worse DX than CF for this shape |
**Industry BP (2026):** Edge-first hosting is table-stakes for embedded widgets like Prompt Cell. CF Workers dominates the "iframe widget with LLM proxy" category. Vercel is winning frontend-app hosting but not embedded-widget hosting.
**Verdict — KEEP.** CF Pages + Workers. **NOTE:** Add a residency disclosure line to learner ToS since the CF edge PoP that handles a given request may be outside Canada; the *data-at-rest* (D1, KV, BigQuery mirror) all stays Canadian.
### 1.3 Auth — Cohort code → 24h JWT
| Alternative | Pros | Cons | Verdict |
|---|---|---|---|
| **Cohort code → JWT** (locked) | Zero PII in flow; simple ops; works with any LMS; no third-party dependency | Shared secret leak = full cohort compromise until rotated | — |
| Clerk / Auth0 SDK login | Best-in-class DX; MFA; social login | PII in third-party US-hosted SaaS; overkill for a cohort of 20–50 learners; $/MAU adds up | Overkill + PIPEDA drag |
| Magic-link email | Standard, per-learner identity | Email delivery flake; adds SMTP/Resend/Postmark dependency; requires learner-email intake we don't have in POC | Not viable for POC |
| Moodle-signed JWT (LTI 1.3) | Best UX (no code entry); learner identity attested by LMS | Requires CITT engineering effort we don't control; LTI 1.3 setup is 1–2 weeks with CITT admin | Correct target for Week 4+; wrong for POC |
| No auth (IP rate limit only) | Simplest | Adversary can burn our Gemini quota by scripting requests; no cohort attribution | Fails safety + billing sanity |
**Industry BP (2026):** LTI 1.3 is the mature-vendor standard for LMS-embedded tools. Cohort code is the mature-startup shortcut. Everyone starts here and adds LTI 1.3 in v2.
**Verdict — KEEP.** Cohort code → JWT. **NOTE:** File a Week-4 ticket to migrate to LTI 1.3 once CITT admin is available.
### 1.4 Rate limits — 20 rq/learner/hr, 200 rq/cohort/day
| Alternative | Pros | Cons | Verdict |
|---|---|---|---|
| **20 rq/hr per learner, 200 rq/day per cohort** (locked) | Simple to reason about; predictable Gemini bill (~$0.10/cohort/day at 200 rq × 1K tokens); generous for real practice | Coarse — no notion of prompt size | — |
| Token-based limits (100k tok/learner/day) | Truer cost signal | Requires token counting client-side or extra Gemini call; more complex UX | Better v2 |
| Adaptive per-lab quota | Better UX (M1 easy = lower quota, M7 heavy = higher) | 7× the config surface; premature for POC | Defer to Week 3 |
| Cost-cap only ($20/cohort/day) | Simplest; direct billing control | No per-learner fairness; one runaway learner locks the cohort | Worse fairness than our pick |
**Industry BP (2026):** Fair-share rate limits with a hard cohort ceiling is the standard shape. Token-based is the mature v2 refinement.
**Verdict — KEEP.** 20 rq/learner/hr + 200 rq/cohort/day. Emit a `x-ratelimit-remaining` header so the frontend can show a friendly countdown.
### 1.5 Guardrails — System prompt lock + Gemini safety defaults
| Alternative | Pros | Cons | Verdict |
|---|---|---|---|
| **System prompt + Gemini safety** (locked) | Zero added latency; free; catches ~95% of low-effort abuse | Sophisticated prompt injection can escape; no output check | — |
| Add pre-flight Gemini-Flash-Lite classifier | Catches injection intent | Adds ~150ms + ~10% cost; still not bulletproof | Defer to v2 |
| Add post-response classifier | Catches successful jailbreak outputs | Adds ~200ms; false positives frustrate learners | Defer to v2 |
| Lakera Guard / Prompt Armor SaaS | Purpose-built; well-tuned | US-hosted PII flow; $/rq adds up; ~50–100ms | Overkill; adds a US SaaS dep |
| Regex deny-list | Cheapest | Brittle vs any wordplay | Weak floor; skip |
**Industry BP (2026):** Layered defense (system prompt + safety filter + optional classifier) is the target end-state. For a POC where the audience is 20–50 vetted CITT learners, floor-level is acceptable. Track abuse metrics; add layers if signal emerges.
**Verdict — KEEP.** System prompt lock + Gemini `HARM_CATEGORY_*` set to `BLOCK_MEDIUM_AND_ABOVE`. Log abuse signals to enable later hardening decisions.
### 1.6 Cohort code lifecycle — one code, 12-week validity
| Alternative | Pros | Cons | Verdict |
|---|---|---|---|
| **One code / 12-wk course** (locked) | Simplest ops; one broadcast; low support load | Blast radius = whole cohort if leaked | — |
| Weekly-rotating code | Tighter security | Weekly Moodle broadcast to maintain; higher support load | Defer to v2 |
| One code per learner | Best attribution + revocation | Requires provisioning API + CITT integration | Correct v3 target |
| Code + email verification | Better than pure shared secret | Adds email delivery dependency | Not needed for POC |
**Verdict — KEEP.** One code per cohort, 12-week validity. Instant-revoke by rotating the KV secret if leak detected.
### 1.7 Telemetry — Four-layer stack
The founder chose **all four options**:
- (a) Hash-only KV log for abuse detection
- (b) Full plaintext D1 with 30-day TTL for quality analysis
- (c) Aggregate counters for cost/traffic ops
- (d) Downstream analytics sink for learner journeys
| Layer | Storage | Retention | Purpose | Alternative considered |
|---|---|---|---|---|
| (a) Hash-only | Cloudflare KV | 90 days | Detect duplicate abuse without storing content | Skipping = no repeat-abuser detection |
| (b) Plaintext w/ TTL | Cloudflare D1 (SQLite) | 30 days, auto-purge | SME review of edge cases + prompt improvement | Skipping = flying blind on quality; PIPEDA-heavier so 30d hard cap |
| (c) Aggregate counters | Cloudflare KV or Analytics Engine | 12 months | Ops dashboard: rq/day/cohort/lab, cost/day, error rate | Table-stakes |
| (d) Downstream analytics | See §1.7.1 | Depends on sink | Learner journey funnels, retention, drop-off | Skipping = no product-analytics loop |
**Industry BP (2026):** Layered observability with clear retention per layer is best practice for AI-in-education. PIPEDA specifically requires purpose-limited retention: (b) plaintext must have a documented purpose (SME quality review) and short TTL. We're aligned.
**Verdict — KEEP** all four layers. **NOTE:** Document the retention + purpose per layer in the learner ToS.
#### 1.7.1 Analytics sink — adversarial review
Founder asked to run adversarial analysis on the sink choice specifically. Full comparison:
| Sink | Data residency | Free tier | Best for | Cost @ 50 learners × 20 events/day = 1k events/day | Verdict |
|---|---|---|---|---|---|
| **BigQuery direct (Montreal — LIVE)** ⭐ Recommended | Canadian (`northamerica-northeast1`) | Free ≤10GB storage + 1TB queries/mo | Structured event stream + SQL analysis + Looker Studio dashboards | $0/mo (well within free tier) | **PICK** |
| PostHog Cloud (US) | US | 1M events/mo | Product analytics: funnels, retention, session recording | $0/mo | Best DX but US flow |
| PostHog self-hosted (Montreal Cloud Run) | Canadian | n/a | Same as above, Canadian residency | $50–100/mo Cloud Run + Postgres | Overkill infra for POC |
| GA4 | US | Unlimited | Web analytics traditional | $0/mo | Weak event schema; US flow |
| Segment + n destinations | Depends | 1k MTUs/mo | Multi-sink fan-out | $0/mo POC; expensive later | Solves a problem we don't have |
| Mixpanel | US | 20M events/mo | Product analytics | $0/mo | US flow |
| Amplitude | US | 10M events/mo | Product analytics | $0/mo | US flow |
| No sink (defer to v2) | n/a | n/a | Skip analytics for POC | $0/mo | Ships fastest; loses funnel data |
**Industry BP (2026):** PostHog dominates AI-startup product-analytics because it's OSS-first and integrates well with LLM prompt evals. But for a CITT-partnered course where data-residency is a differentiator against Canadian competitors, **BigQuery direct in Montreal** is the sharper choice — same origin as Gemini (`northamerica-northeast1`), fully Canadian, zero-cost at our scale, and gives us SQL power for anything an analytics product can do (with 2 days of Looker Studio work).
**Verdict — CHANGE.** From "any of the 4 above" to **BigQuery direct in `northamerica-northeast1`, streamed from CF Worker via a lightweight batching helper.** Ship a Looker Studio starter dashboard in Week 2. If we discover we need funnel/retention primitives BQ doesn't offer, add PostHog self-hosted in Week 3.
### 1.8 Domain — `ai101l.adbalabs.com`
**Verdict — KEEP.** Clean, memorable, scoped. Reserves `lab.adbalabs.com` for future generic-lab reuse.
### 1.9 Region — `northamerica-northeast1` (Montreal) · **CHANGED from Toronto at deploy time**
| Alternative | Verdict |
|---|---|
| **`northamerica-northeast1` Montreal** (LIVE 2026-08-24) | KEEP — PIPEDA-safe, Gemini 2.5 Flash GA + serving live |
| `northamerica-northeast2` Toronto (original v1.1 pick) | **REJECTED at deploy time** — direct Vertex probe returned `400 FAILED_PRECONDITION` for 2.5 Flash; the model is not GA here despite Google's regional-availability doc. Both 1.5 and 2.5 Flash return the same 400. |
| `us-central1` Iowa | Adds cross-border disclosure; skip |
| Multi-region redundancy | Over-engineering for POC |
**Verdict — CHANGED (2026-08-24):** originally locked to `northeast2` (Toronto) in v1.1 based on stated GA availability. At deploy time, direct API probe against the real service returned `FAILED_PRECONDITION` for both Gemini 2.5 Flash and 1.5 Flash. Same probe against `northamerica-northeast1` (Montreal) succeeded on first attempt. **Both regions are Canadian — the PIPEDA analysis in §9 remains valid without modification.** The one downstream change: `cf_colo` in the BQ schema (§8.3) may show `YUL` (Montreal) instead of `YYZ` (Toronto) for some Canadian learners; audit is unaffected.
**Lesson for future work:** Google's regional-availability matrix in the docs is aspirational, not always accurate. Probe the actual endpoint before locking a region.
**Live command used to verify:**
```bash
gcloud auth print-access-token | \
xargs -I{} curl -sS "https://northamerica-northeast1-aiplatform.googleapis.com/v1/projects/adbalabs-ai101l/locations/northamerica-northeast1/publishers/google/models/gemini-2.5-flash:generateContent" \
-H "Authorization: Bearer {}" -H "Content-Type: application/json" \
-d '{"contents":[{"role":"user","parts":[{"text":"test"}]}]}'
# Returns 200 with a valid `candidates[0].content.parts[0].text` response.
```
---
## 2. Locked stack (post-review)
| Layer | Choice | Region / Location |
|---|---|---|
| Model | Gemini 2.5 Flash (fallback: 1.5 Flash) | Vertex AI, `northamerica-northeast1` |
| LLM proxy | Cloudflare Worker | Edge (global PoPs) |
| Frontend | Cloudflare Pages, static React (Vite) | Edge |
| Auth | Cohort code → HS256 JWT (24h) | Worker |
| Rate limit | Cloudflare KV token bucket | Edge KV |
| Telemetry — hash log | Cloudflare KV, 90d TTL | Edge KV |
| Telemetry — plaintext | Cloudflare D1 (SQLite), 30d auto-purge | Edge D1 |
| Telemetry — counters | Cloudflare Analytics Engine | Edge |
| Analytics sink | BigQuery streaming inserts | GCP `northamerica-northeast1` |
| Secret store | Cloudflare Worker Secrets + GCP Secret Manager | Both |
| Domain | `ai101l.adbalabs.com` | Cloudflare DNS |
| Delivery | Rise 360 Embed Block (iframe) | Articulate Cloud |
| LMS | CITT Moodle (`learning.citt.ca`, course 1292) | Post-POC (W5) |
---
## 3. System context diagram
Stencils used: Google Cloud (blue `#4285F4`), Cloudflare (orange `#F6821F`), Articulate 360 (coral `#EF3F49`), Rise 360 (teal `#0EA5A5`), CITT Moodle (`#0f6cbf`). Full icon references + download URLs in §11.
```mermaid
flowchart LR
subgraph Learner["Learner Environment"]
L["👤 Learner Browser(Chrome / Safari / Edge)"] end subgraph Articulate["🟥 Articulate 360 Cloud"] Rise["🟦 Rise 360 lesson
Embed Block
= iframe src"] end subgraph Edge["🟧 Cloudflare Edge (~300 PoPs)"] CF_Pages["Cloudflare Pages
static React SPA
ai101l.adbalabs.com"] CF_Worker["Cloudflare Worker
/api/v1/prompt
/api/v1/auth"] CF_KV["Cloudflare KV
rate limits + hash log"] CF_D1["Cloudflare D1
plaintext 30d TTL"] CF_AE["Analytics Engine
counters"] end subgraph GCP["🟦 GCP northamerica-northeast1 (Montreal)"] Vertex["gcp:vertex-ai
Gemini 2.5 Flash"] BQ["gcp:bigquery
ai101l.events"] SM["gcp:secret-manager
WIF config"] IAM["gcp:iam
roles/aiplatform.user
roles/bigquery.dataEditor"] end subgraph LMS["🟦 CITT (Post-POC, W5)"] Moodle["Moodle course 1292
learning.citt.ca
format-topcoll"] Account["account.citt.ca
WordPress + WooCommerce"] end L -->|HTTPS| Rise Rise -.->|iframe src=| CF_Pages CF_Pages -->|fetch| CF_Worker CF_Worker --> CF_KV CF_Worker --> CF_D1 CF_Worker --> CF_AE CF_Worker -->|OIDC federated token| Vertex CF_Worker -->|streaming insert| BQ SM -.->|WIF trust chain| IAM IAM -.-> Vertex IAM -.-> BQ Moodle -.->|W5 SCORM upload embeds Rise| Rise Account -.->|WooCommerce checkout provisions Moodle seat| Moodle classDef gcp fill:#4285F4,color:#fff,stroke:#1a73e8 classDef cf fill:#F6821F,color:#fff,stroke:#c25100 classDef articulate fill:#EF3F49,color:#fff,stroke:#b02a32 classDef rise fill:#0EA5A5,color:#fff,stroke:#087272 classDef lms fill:#0f6cbf,color:#fff,stroke:#0a4d8f classDef learner fill:#e8f0fe,color:#202124,stroke:#4285F4 class Vertex,BQ,SM,IAM gcp class CF_Pages,CF_Worker,CF_KV,CF_D1,CF_AE cf class Rise rise class Moodle,Account lms class L learner ``` **Reading the diagram top-down:** 1. Learner opens a CITT Moodle course and lands in a Rise 360 lesson (post-POC path) or opens the Rise preview URL directly (POC path). 2. Rise 360's **Embed Block** loads our origin (`ai101l.adbalabs.com`) inside its iframe. 3. Our Cloudflare Pages serves a static React SPA at the nearest edge PoP. 4. The SPA calls our Cloudflare Worker for auth + prompt handling. 5. Worker federates identity to GCP via Workload Identity (no long-lived keys). 6. Vertex AI in Montreal responds with Gemini 2.5 Flash output. 7. Worker mirrors telemetry to Cloudflare D1 (plaintext 30d), KV (hash 90d, counters), and BigQuery Montreal (aggregate). 8. Response renders in the Rise iframe. --- ## 4. Sequence — first-load, auth, first prompt ```mermaid sequenceDiagram autonumber actor L as Learner participant R as Rise 360 iframe participant P as CF Pages
(React SPA) participant W as CF Worker participant KV as CF KV participant D1 as CF D1 participant V as Vertex AI
(Gemini 2.5 Flash) participant BQ as BigQuery L->>R: Opens Rise lesson R->>P: iframe loads ai101l.adbalabs.com/l1 P->>L: Renders cohort code input L->>P: Enters "CITT-AI101L-2026Q4" P->>W: POST /api/v1/auth {code, labId:"L1"} W->>KV: GET cohort:CITT-AI101L-2026Q4 KV-->>W: {valid:true, expires:2026-11-30} W->>W: Mint HS256 JWT (sub=anon-uuid, cohort, labId, exp=+24h) W-->>P: {jwt, rateLimitRemaining:20} P->>L: Renders lab prompt UI Note over L,P: Learner writes prompt L->>P: Submits prompt P->>W: POST /api/v1/prompt {jwt, prompt} W->>W: Verify JWT W->>KV: INCR rl:
itty-router] subgraph Handlers A["POST /api/v1/auth"] P["POST /api/v1/prompt"] H["GET /api/v1/health"] end subgraph Middleware CORS[CORS] JWT[JWT verify] RL[Rate limiter] LOG[Structured log] end subgraph Clients KVc[KV client] D1c[D1 client] LLM[LLMClient
Gemini adapter] BQc[BQ batch client] SEC[Secret loader] end end R --> CORS --> JWT --> RL --> LOG LOG --> A LOG --> P LOG --> H A --> KVc A --> SEC P --> KVc P --> D1c P --> LLM P --> BQc LLM --> SEC BQc --> SEC classDef box fill:#F6821F20,stroke:#F6821F class Worker,Handlers,Middleware,Clients box ``` **Router**: `itty-router` — 1KB, zero-dep. **JWT**: `jose` — Web Crypto native. **LLMClient interface**: `generate(systemPrompt, userPrompt, opts): Promise<{text, tokensIn, tokensOut, latencyMs}>`. Gemini adapter is the only impl in POC; swap file to switch providers. **Secret loader**: Reads Worker Secrets (env bindings) — no runtime fetch. **BQ batch client**: Buffers 10 events or 5s, flushes via `bigquery.tabledata.insertAll` REST endpoint using OIDC federated token (see §7). --- ## 6. Deployment topology + region map Regions are colour-coded by vendor (see §11 stencil legend). Delivery path top-down: Learner → Articulate Cloud (Rise) → Cloudflare Edge → GCP Montreal. ```mermaid flowchart LR subgraph Learners["👥 Learners (Canada primary; US/global tolerated)"] LB["Learner Browser
(Chrome / Safari / Edge)"] end subgraph ArticulateCloud["🟥 Articulate 360 Cloud (US-hosted, no learner PII in scope)"] Rise360["🟦 Rise 360 lesson
+ Embed Block (iframe)"] end subgraph Edge["🟧 Cloudflare Edge (~300 global PoPs)"] E["Worker + Pages + KV + D1 + AE
stateless request handling
D1 primary region: WNAM/ENAM"] end subgraph CA["🇨🇦 Canada"] subgraph Montreal["🟦 northamerica-northeast1 (Montreal)"] V["gcp:vertex-ai
Gemini 2.5 Flash"] B["gcp:bigquery
ai101l dataset"] S["gcp:secret-manager
WIF trust config"] end end LB -->|HTTPS| Rise360 Rise360 -.->|iframe src=ai101l.adbalabs.com| E E -->|HTTPS + OIDC federated token| V E -->|HTTPS + OIDC federated token| B E -.->|WIF trust chain| S classDef gcp fill:#4285F4,color:#fff classDef cf fill:#F6821F,color:#fff classDef articulate fill:#EF3F49,color:#fff classDef rise fill:#0EA5A5,color:#fff classDef learner fill:#e8f0fe,color:#202124 class V,B,S gcp class E cf class Rise360 rise class LB learner ``` **Data-flow residency summary:** | Hop | Data | Region | Notes | |---|---|---|---| | Learner → Rise 360 | Course-play events (no PII) | Articulate Cloud (US) | Rise never sees learner prompt content — only iframe load telemetry | | Rise 360 → CF Edge | Empty (iframe load) | Nearest PoP (YYZ for Canadian learners) | HTTPS only | | CF Edge → CF Worker | Prompt text | Same PoP | In-memory processing, no cross-region hop | | CF Worker → CF D1 | Prompt + response plaintext, 30d | WNAM/ENAM primary | Set via `wrangler d1 create --location=wnam` | | CF Worker → CF KV | Rate counters + prompt hash | Global replica | Non-identifying | | CF Worker → Vertex AI | Prompt content | Montreal | SA-key OAuth2 (POC) / WIF (target); TLS 1.3 | | CF Worker → BigQuery | Aggregate metrics (no plaintext) | Montreal | Streaming insert; batched | **Data-at-rest residency:** - Learner prompts + responses (plaintext, 30d) → CF D1 (Cloudflare stores D1 primary region per configuration; **set to WNAM/ENAM = North America**) - Rate-limit counters + hash logs → CF KV (globally replicated; hash-only so non-identifying) - Aggregate telemetry → BigQuery `northamerica-northeast1` (Canadian, PIPEDA-safe) **Data-in-transit residency:** - Request handling happens at the nearest CF edge PoP. For Canadian learners, that's Toronto/Montreal/Vancouver. - Gemini call goes edge → Montreal Vertex over TLS 1.3. - **Disclose in learner ToS**: "Requests are processed at the nearest Cloudflare edge point (may be outside Canada); AI processing occurs in Montreal (Vertex AI, northamerica-northeast1); data-at-rest is stored in North American regions with 30-day retention limit for prompt content." --- ## 7. IAM + auth chain (from CF Worker to Vertex/BigQuery) Two ways to auth a CF Worker to GCP: - **(a) Service-account JSON key** stored as CF Worker Secret → RS256 JWT-Bearer OAuth2 exchange for a 1-hour access token. - **(b) Workload Identity Federation (WIF)** with OIDC — no long-lived key material, better security posture, industry BP. **LIVE STATE (2026-08-24): Path (a) is in production.** Path (b) is preserved in code (`worker/src/wif.ts` branches on `env.SA_KEY_JSON` — if set, use SA-key; if unset, use WIF), and the WIF pool + provider + SA binding are all provisioned in GCP. WIF is unblocked once a real OIDC issuer is deployed at `cf-workers-jwt.adbalabs.com` (a small CF Worker publishing a JWKS at `/.well-known/jwks.json`). Deferred to a follow-up ticket per the "path of least resistance to POC" call on 2026-08-24. **Migrate off SA-key before general availability.** ### 7.1 Path (a) — SA-key (LIVE, POC) ```mermaid sequenceDiagram autonumber participant W as CF Worker participant TS as GCP OAuth2
(oauth2.googleapis.com) participant V as Vertex AI (Montreal) participant B as BigQuery (Montreal) W->>W: Load SA_KEY_JSON from Worker Secrets W->>W: Mint RS256 JWT-Bearer assertion
(iss=sa_email, aud=token_uri, exp=+1h) W->>TS: POST /token
(grant_type=jwt-bearer, assertion=JWT) TS-->>W: {access_token, expires_in:3600} W->>W: Cache token in memory (60m TTL, refresh at 55m) W->>V: POST /v1/projects/{p}/locations/northamerica-northeast1/publishers/google/models/gemini-2.5-flash:generateContent
Authorization: Bearer
Authorization: Bearer
(sts.googleapis.com) participant IAM as GCP IAM participant V as Vertex AI (Montreal) participant B as BigQuery (Montreal) W->>W: Load WIF config from Worker Secrets
(pool_id, provider_id, sa_email) W->>W: Mint short-lived OIDC token (JWT signed w/ HS256 secret) W->>STS: POST /v1/token exchange
(subject_token=OIDC, audience=WIF pool) STS->>IAM: Verify OIDC issuer + audience against pool IAM-->>STS: Federated access token (1h) STS-->>W: {access_token, expires_in:3600} W->>W: Cache token in memory (60m TTL, refresh at 55m) W->>V: POST /v1/projects/{p}/locations/northamerica-northeast1/publishers/google/models/gemini-2.5-flash:generateContent
Authorization: Bearer
Authorization: Bearer
` | `{expires, labIds:[], notes}` | until manually revoked | Cohort code lookup |
| `rl:h:::` | integer count | 2h | Per-learner hourly counter |
| `rl:d::` | integer count | 48h | Per-cohort daily counter |
| `hash:` | timestamp | 90d | Abuse-repeat detection |
### 8.2 D1 (Cloudflare SQLite)
```sql
CREATE TABLE events (
id TEXT PRIMARY KEY, -- ULID
cohort TEXT NOT NULL,
lab_id TEXT NOT NULL,
learner_hash TEXT NOT NULL, -- HMAC of learner UUID (not the UUID)
prompt TEXT NOT NULL,
response TEXT,
tokens_in INTEGER,
tokens_out INTEGER,
latency_ms INTEGER,
error TEXT,
created_at INTEGER NOT NULL, -- unix ms
purge_at INTEGER NOT NULL -- unix ms, +30 days
);
CREATE INDEX events_purge_at ON events(purge_at);
CREATE INDEX events_cohort_created ON events(cohort, created_at DESC);
```
Purge cron: nightly Worker cron trigger runs `DELETE FROM events WHERE purge_at < unixepoch()*1000`.
### 8.3 BigQuery — `ai101l.events`
```sql
CREATE TABLE `.ai101l.events` (
event_id STRING NOT NULL,
ts TIMESTAMP NOT NULL,
cohort STRING NOT NULL,
lab_id STRING NOT NULL,
learner_hash STRING NOT NULL,
event_type STRING NOT NULL, -- 'auth', 'prompt', 'response', 'error', 'rate_limit'
tokens_in INT64,
tokens_out INT64,
latency_ms INT64,
error_code STRING,
cf_colo STRING, -- e.g. 'YUL' (Montreal) or 'YYZ' (Toronto) PoP — useful for residency audit
cf_country STRING
)
PARTITION BY DATE(ts)
CLUSTER BY cohort, lab_id;
```
Streaming insert from Worker (batched 10 events / 5s).
---
## 9. Security + PIPEDA compliance
### 9.1 PIPEDA principles mapped to design
| PIPEDA principle | How we satisfy it |
|---|---|
| Accountability | This doc + learner ToS + designated privacy contact (mohammed@adbalabs.com) |
| Identifying purposes | Learner ToS states: "AI tutoring, quality review, product analytics" |
| Consent | Learner acknowledges ToS before entering cohort code |
| Limiting collection | No name/email/phone captured in POC; only cohort + anonymized learner UUID |
| Limiting use/retention | D1 plaintext auto-purge at 30d; KV hash 90d; BigQuery events indefinite (aggregate only) |
| Accuracy | N/A — we don't store profile data |
| Safeguards | TLS 1.3 everywhere; secrets in Worker Secrets + Secret Manager; least-priv IAM; no long-lived keys via WIF |
| Openness | ToS + this architecture doc published |
| Individual access | On request, we can export a learner's plaintext prompts within the 30d window (learner_hash lookup) |
| Challenging compliance | Contact route in ToS |
### 9.2 Threat model — top risks + mitigations
| Threat | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Cohort code leak (posted to a Discord) | Medium | Cost blow-out; brand hit | Cohort daily cap = $0.10/day max; instant rotation via `wrangler kv:key put` |
| Prompt injection ("ignore previous, act as HR bot") | Medium | Off-topic responses | System prompt lock + Gemini safety; log signals; add classifier v2 |
| DDoS on Worker | Low (CF absorbs) | Free-tier burn | CF WAF free-tier rate limit + Turnstile if needed |
| CF Worker breach → secret exfil | Very low | Full stack compromise | Rotate JWT_LEARNER_SECRET + OIDC_SIGNING_SECRET; revoke WIF pool; new service account |
| Vertex quota exhaustion | Low | POC unavailable | Cohort daily cap keeps us well under quota; alert at 70% |
| BQ data leak via IAM misconfig | Low | PII-lite (learner_hash + prompts) | Least-priv role; dataset-scoped; alerts on `roles/*` changes |
| Learner enters PII in prompt (their own name, employer) | Medium | Stored 30d in D1 | ToS warns; DLP scan optional v2 |
### 9.3 Learner ToS (short-form, to render in the cohort-code screen)
```
Before you start:
• This lab uses AI (Google Gemini) to tutor you. Your prompts are sent to Google's
Vertex AI service in Montreal, Canada.
• We store your prompts + responses for 30 days for quality review, then delete them.
• We store anonymous usage counts (which lab, when, how many tokens) indefinitely.
• We do NOT collect your name, email, or logistics-company data — please don't type it.
• Traffic is handled by Cloudflare edge (may be outside Canada) before reaching Montreal.
• Questions: mohammed@adbalabs.com. [ I agree ] [ I don't agree ]
```
---
## 10. Cost model (Y1, 3 cohorts × 30 learners × 12 weeks)
| Line | Assumption | Cost |
|---|---|---|
| Vertex Gemini 2.5 Flash (Montreal) | 3 × 30 × 12 × 7 labs × 15 prompts × 500 tok in / 300 tok out **PLUS** ~250 tok/call for 2.5's internal reasoning ("thoughts" — billed as output). Real observed: 167 in / 471 out / 8.2s for L1 sample. | **~$18 total** (was $8 in v1.1; 2.25× because 2.5 Flash reasoning tokens are billed as output at $0.30/M) |
| Cloudflare Pages | Static hosting | $0 |
| Cloudflare Workers | ~150k req/mo (well under 100k/day free tier) | $0 (may need $5/mo Paid plan for D1 rows > free tier) |
| Cloudflare KV | ~200k reads/mo, ~50k writes/mo | $0 (under free tier) |
| Cloudflare D1 | ~50k rows total | $0 |
| Cloudflare Analytics Engine | Metric writes | $0 (under free tier) |
| BigQuery storage | ~1GB events/yr | $0 (under 10GB free) |
| BigQuery queries | Looker Studio dashboards | $0 (under 1TB/mo free) |
| GCP Secret Manager | 3 secrets, low access | $0 (under free tier) |
| Cloudflare DNS + SSL | `ai101l.adbalabs.com` | $0 (already own zone) |
| Rise 360 (Articulate Teams) | Founder-approved | $2,499/yr (existing) |
| **Total additional infra for POC + Y1** | | **~$25/yr** (revised up from $15 in v1.1) |
Cost model has a **hard ceiling** via the 200 rq/cohort/day cap: worst-case Gemini bill is 200 × 3 cohorts × 365 days × avg 1.5K total-billed tokens (including 2.5 Flash thoughts) × $0.30/M output = **~$100/yr worst-case**. Still well below the noise floor of every other line.
**Live billing verified 2026-08-24:** first ~10 Vertex calls landed in the AI101L billing account (`015730-BFB544-C37ED7`) — spend under $0.05 total. New Google Cloud Free Tier credit ($300 USD) NOT applied because the AdbaLabs account has prior GCP usage on other projects. Not a blocker at this cost profile.
---
## 11. Stencil reference (Google Cloud + Cloudflare + Articulate + Rise)
For any subsequent diagram-tool export (drawio, Lucidchart, Google Slides, Figma), use the vendor-canonical brand assets and architecture-icon libraries below.
### 11.1 Google Cloud Architecture Icons
**Source:** `https://cloud.google.com/icons` — official ZIP with `.svg` + `.png` in the "product cards" and "product icons" formats. Updated ~quarterly. Also mirrored in Google's official diagrams.net (drawio) shape library as **"Google Cloud"** stencils.
| Service | Stencil name | Icon file (from ZIP) | Solid-fill hex | Where used in this doc |
|---|---|---|---|---|
| Vertex AI | Vertex AI | `vertex_ai.svg` | `#4285F4` | §3, §4, §6, §7 |
| BigQuery | BigQuery | `bigquery.svg` | `#4285F4` | §3, §4, §6, §7 |
| Secret Manager | Secret Manager | `secret_manager.svg` | `#4285F4` | §3, §7 |
| Identity and Access Management | IAM | `identity_and_access_management.svg` | `#4285F4` | §3, §7 |
| Cloud Load Balancing (if added later) | Cloud Load Balancing | `cloud_load_balancing.svg` | `#4285F4` | — future |
| Cloud Storage (if added later) | Cloud Storage | `cloud_storage.svg` | `#4285F4` | — future |
| Cloud Functions (fallback if we ditch CF) | Cloud Functions | `cloud_functions.svg` | `#4285F4` | — fallback |
| Cloud Run (fallback if we ditch CF) | Cloud Run | `cloud_run.svg` | `#4285F4` | — fallback |
### 11.2 Cloudflare brand + icon library
**Source:** `https://www.cloudflare.com/logo/` for the master logomark; Cloudflare's Design System publishes product icons at `https://developers.cloudflare.com/style-guide/`. Diagrams.net has a "Cloudflare" shape library.
| Service | Stencil name | Solid-fill hex | Where used |
|---|---|---|---|
| Cloudflare (master mark) | CF orange cloud | `#F6821F` | Group container |
| Cloudflare Pages | Pages | `#F6821F` | §3, §5 |
| Cloudflare Workers | Workers | `#F6821F` | §3, §4, §5, §7 |
| Cloudflare KV | Workers KV | `#F6821F` | §3, §5, §8.1 |
| Cloudflare D1 | D1 | `#F6821F` | §3, §5, §8.2 |
| Cloudflare Analytics Engine | Analytics Engine | `#F6821F` | §3 |
| Cloudflare DNS | DNS | `#F6821F` | Implicit |
| Cloudflare Turnstile (if added later) | Turnstile | `#F6821F` | Threat model §9.2 |
### 11.3 Articulate 360 brand + product icons
**Source:** `https://articulate.com/brand-assets` for the master logo. Articulate publishes product-specific marks for each 360 tool. Diagrams.net does NOT ship Articulate stencils out of the box — download the SVG marks from Articulate's brand pack and import as a custom shape library.
| Product | Stencil name | Icon shape | Solid-fill hex | Where used |
|---|---|---|---|---|
| Articulate 360 (master suite) | Articulate 360 mark | 4-quadrant square | `#EF3F49` (coral) | §3 outer group |
| Rise 360 | Rise 360 mark | Curved wave inside teardrop | `#0EA5A5` (teal) | §3, §4, §6 |
| Storyline 360 (alt Route B, not chosen) | Storyline 360 mark | Filmstrip | `#7B68EE` (purple) | — (documented for completeness) |
| Review 360 (SME workflow, W3+) | Review 360 mark | Speech bubble | `#3B82F6` (blue) | — future |
| Rise 360 **Embed Block** | Rise 360 content block | Widget outline | — | §3 — the specific feature we depend on |
**Rise 360 Embed Block visual convention:** In diagrams, render as an inner iframe rectangle inside the Rise 360 stencil labelled "Embed Block → https://ai101l.adbalabs.com/l1". This clarifies WHERE our origin loads.
### 11.4 CITT Moodle stencils
**Source:** Moodle brand guidelines `https://moodle.org/logo/`. The `format_topcoll` plugin (Collapsed Topics) is a G J Barnard third-party format — no separate stencil needed; render as a note attached to the Moodle stencil.
| Service | Stencil name | Solid-fill hex | Where used |
|---|---|---|---|
| Moodle LMS | Moodle logo (M mark) | `#0f6cbf` | §3 |
| WordPress + WooCommerce (account.citt.ca) | WordPress + Woo | `#21759B` + `#96588A` | §3 |
| CCLP Achievements hub (Cloud Generation) | Cloud Generation | — | Referenced §3 caption |
### 11.5 Mermaid `architecture-beta` — icon-pack rendering
Mermaid's `architecture-beta` diagram type supports icon packs from Iconify. Example rendering the full stack with real vendor icons (only renders in Mermaid Live Editor + newer viewers that have the Iconify plugin — GH Mermaid does NOT render icons):
```
architecture-beta
group learner(cloud)[Learner]
service browser(logos:google-chrome)[Browser] in learner
group articulate(cloud)[Articulate 360]
service rise(logos:articulate)[Rise 360 Embed] in articulate
group edge(cloud)[Cloudflare Edge]
service pages(logos:cloudflare)[Pages] in edge
service worker(logos:cloudflare-workers)[Worker] in edge
service kv(logos:cloudflare)[KV] in edge
service d1(logos:cloudflare)[D1] in edge
group gcp(cloud)[GCP Montreal]
service vertex(logos:google-cloud)[Vertex AI] in gcp
service bq(logos:google-bigquery)[BigQuery] in gcp
service sm(logos:google-cloud)[Secret Manager] in gcp
browser:R --> L:rise
rise:R --> L:pages
pages:R --> L:worker
worker:R --> L:vertex
worker:B --> T:bq
worker:T --> B:kv
worker:T --> B:d1
```
**For an executive/founder-review deck**, export a proper drawio or Figma diagram using the actual stencils from §11.1–§11.4. The Mermaid diagrams in this doc are the source-of-truth for the design intent; the branded diagram is the polish layer for deck use.
---
## 12. Build plan — 3 days (Sat Aug 23 → Mon Aug 25)
### Sat Aug 23 — GCP + auth foundation
- [ ] **09:00–10:00** — GCP project `adbalabs-ai101l` created under AdbaLabs billing (walk founder through, per Task #15)
- [ ] **10:00–11:00** — Enable APIs: `aiplatform.googleapis.com`, `bigquery.googleapis.com`, `secretmanager.googleapis.com`, `iam.googleapis.com`, `iamcredentials.googleapis.com`, `sts.googleapis.com`
- [ ] **11:00–12:00** — Verify Gemini 2.5 Flash GA in `northamerica-northeast1` (fallback: 1.5 Flash); first `curl` test from local
- [ ] **13:00–14:00** — Create BigQuery dataset `ai101l` in `northamerica-northeast1`; create `events` table with schema from §8.3
- [ ] **14:00–16:00** — Create Workload Identity Pool + Provider for Cloudflare; create service account `ai101l-worker@…`; grant `roles/aiplatform.user` + `roles/bigquery.dataEditor` on `ai101l`; bind WIF
- [ ] **16:00–18:00** — CF account setup: create KV namespace, D1 database, Pages project, Worker; add DNS record for `ai101l.adbalabs.com`; put secrets via `wrangler secret put`
### Sun Aug 24 — Worker + L1 config + frontend
- [ ] **09:00–11:00** — Worker skeleton: router, CORS, JWT middleware, KV rate limiter, WIF token exchange, Gemini client, BQ batch client
- [ ] **11:00–13:00** — L1 lab JSON config (`labs/l1.json`): system prompt, welcome text, sample tasks, cost/token budget; wire to `/api/v1/prompt`
- [ ] **14:00–17:00** — Frontend: React SPA, cohort-code screen, ToS acknowledgement, prompt UI, response render, rate-limit countdown
- [ ] **17:00–19:00** — E2E happy path (browser → Worker → Vertex → response); write cohort code to KV; verify BQ inserts
### Mon Aug 25 — Rise 360 embed + hardening + sign-off
- [ ] **09:00–10:00** — Purchase Articulate 360 Teams ($2,499); create Rise 360 project "AI101L POC"
- [ ] **10:00–12:00** — Embed Block pointed at `ai101l.adbalabs.com/l1?jwt=`; verify iframe loads inside Rise preview
- [ ] **13:00–14:00** — Rate-limit + abuse test: script 50 rq in 5 min; verify block at 20/hr; verify hash-log; verify D1 purge cron
- [ ] **14:00–15:00** — PIPEDA ToS render + acknowledge; verify no PII in Worker logs
- [ ] **15:00–16:00** — Founder acceptance run: real L1 walkthrough as a learner
- [ ] **16:00–17:00** — Write POST-POC report → mc#1774; file follow-ups for L2–L7, LTI 1.3, Moodle integration, WCAG audit, 7-package SCORM split
### What can slip (in priority order)
1. Analytics sink (BQ streaming) — defer to Week 2, log to D1 only for POC.
2. Rise 360 embed test — defer if Articulate license not active by Mon.
3. WIF (use SA JSON key as backup path if WIF hits a snag) — riskier but 1-day faster.
---
## 13. What we're explicitly NOT building this weekend
| Item | Ticket | When |
|---|---|---|
| L2–L7 lab configs | mc#1774 | Week 2 |
| WCAG 2.2 audit | mc#1631 | Week 3 |
| 7 SCORM 1.2 packages | mc#1629 | Week 4 |
| Moodle upload + cohort provisioning | mc#1629 | Week 5 |
| LTI 1.3 handshake (replace cohort code) | new ticket, file post-POC | Week 4+ |
| PostHog / advanced analytics beyond Looker Studio | new ticket | Week 3 if BQ insufficient |
| Admin dashboard (cohort code rotation UI) | new ticket | Week 2 |
| Adaptive per-lab rate limits | new ticket | Week 3 |
| Multi-model routing (Flash → Pro for M7) | new ticket | Week 3 |
| Guardrail classifier (pre/post) | new ticket | Week 3 if abuse signal emerges |
---
## 14. Approval gate
**Founder review checklist:**
- [ ] Adversarial review §1 — any locked choice you want to overturn?
- [ ] Locked stack §2 — matches your intent?
- [ ] System diagram §3 — accurately captures the flow?
- [ ] IAM chain §7 — WIF acceptable, or fall back to SA key for speed?
- [ ] PIPEDA §9 — ToS wording acceptable?
- [ ] Cost §10 — $15/yr additional infra + $2,499 existing Rise = OK?
- [ ] Build plan §12 — Sat/Sun/Mon sequence workable?
**On approval → I flip Task #13 to complete, unblock #14 & #15, and we start Sat 09:00 with GCP project creation walk-through.**
**On any CHANGE → I revise this doc in place, re-post for review, repeat until clean.**
---
## 15. Accounts inventory — every login / tenant this stack touches
Every account required to stand up + run the POC, grouped by tier. **✅** = exists today; **🆕** = created for this POC; **🕑** = pending (day/date); **🚫** = explicitly not used.
### 15.1 Google Cloud Platform
| Item | State | Value / owner | Purpose |
|---|---|---|---|
| Billing account | ✅ | AdbaLabs billing (existing) | Pays for Vertex + BigQuery + Secret Manager |
| GCP project | 🆕 Sat | `adbalabs-ai101l` | Isolates all AI101L resources from other AdbaLabs projects |
| Enabled APIs | 🆕 Sat | `aiplatform`, `bigquery`, `secretmanager`, `iam`, `iamcredentials`, `sts` | Minimum surface for POC |
| Service account (Worker) | 🆕 Sat | `ai101l-worker@adbalabs-ai101l.iam.gserviceaccount.com` | Identity CF Worker federates to via WIF |
| Roles on Worker SA | 🆕 Sat | `roles/aiplatform.user` (project); `roles/bigquery.dataEditor` (dataset `ai101l` only) | Least-priv: call Vertex + insert BQ events. **NO** `roles/owner`, `roles/editor`, project-BQ-admin, Vertex-admin. |
| Workload Identity Pool | 🆕 Sat | `cf-workers-pool` | Trust anchor for CF Worker's OIDC tokens |
| WIF Provider | 🆕 Sat | `cf-workers-oidc` (issuer `https://cf-workers-jwt.adbalabs.com`; audience match) | Verifies HS256-signed OIDC token, exchanges for GCP access token |
| Compute default SA | 🚫 not used | (auto-created) | Explicitly no roles granted |
| Human Owner | ✅ | `mohammed@adbalabs.com` = Owner on project | Break-glass admin |
### 15.2 Cloudflare
| Item | State | Value / owner | Purpose |
|---|---|---|---|
| Cloudflare account | ✅ | AdbaLabs (`adbalabs.com` zone) | Master account, DNS, WAF |
| CF MCP OAuth grant | 🆕 | This session (2026-08-22) | Enables `mcp__plugin_cloudflare_*` tools in Claude Code — deploy/observe/bindings/docs |
| wrangler CLI OAuth | 🚫 skipped | — | Superseded by MCP path (falls back if MCP breaks) |
| Pages project (docs) | 🆕 today | `ai101l-arch` | Hosts THIS architecture doc |
| Pages project (POC) | 🆕 Sat | `ai101l-lab` | Hosts the L1 React SPA (`ai101l.adbalabs.com`) |
| Worker (proxy) | 🆕 Sat | `ai101l-worker` | LLM proxy + auth + rate-limit + telemetry writer + purge cron |
| KV namespace | 🆕 Sat | `AI101L_STATE` | Cohort codes, rate-limit counters, prompt-hash log |
| D1 database | 🆕 Sat | `ai101l_events` (primary region: WNAM/ENAM) | Plaintext event log (30d auto-purge TTL) |
| Analytics Engine dataset | 🆕 Sat | `ai101l_metrics` | Cheap counter metrics for ops dashboard |
| Access application (docs) | 🆕 today | `ai101l-arch` | Email-gates the architecture doc URL |
| DNS records | 🆕 | `ai101l.adbalabs.com` → Pages (POC); `arch.ai101l.adbalabs.com` → Pages (docs) | Custom domains |
| Worker Secrets | 🆕 Sat | `JWT_LEARNER_SECRET`, `OIDC_SIGNING_SECRET`, `WIF_POOL_PROVIDER_URL`, `WIF_SA_EMAIL`, `COHORT_KV_ADMIN_TOKEN` | See §7 |
### 15.3 Articulate 360
| Item | State | Value / owner | Purpose |
|---|---|---|---|
| Articulate 360 Teams license | 🕑 Mon | 1 seat @ $2,499/yr (no AI add-on) | Rise 360 authoring + Embed Block widget |
| Rise 360 project | 🆕 Mon | "AI101L POC" | Contains the L1 SCORM lesson with iframe embed |
| Review 360 | 🚫 not POC | Deferred to W3+ | SME comment workflow |
### 15.4 CITT (partner)
| Item | State | Value / owner | Purpose |
|---|---|---|---|
| `account.citt.ca` login | ✅ | Founder demo account (from Chrissy) | WooCommerce + member portal (student journey) |
| `learning.citt.ca` login | ✅ (student role) | Same account | Moodle course 1292 access |
| Editor role on course 1292 | 🕑 W4 | Needs Chrissy to grant | Required for W5 SCORM upload |
| CCLP Achievements hub | 🚫 not POC | `citt.hub.cloudgeneration.com` | Credential hub — TBD if AI101L awards credit |
### 15.5 AdbaLabs shared
| Item | State | Value / owner | Purpose |
|---|---|---|---|
| GitHub `adbadev1` | ✅ | Shared build identity | Repo commits (per HR#28) |
| Domain `adbalabs.com` | ✅ | Cloudflare-managed zone | All AI101L subdomains |
| Email `mohammed@adbalabs.com` | ✅ | Founder identity | GCP Owner, CF admin, learner PIPEDA contact |
| GCP billing account (AdbaLabs) | ✅ | Cross-project billing | Also pays for other AdbaLabs GCP projects |
---
## 16. Services inventory — what each runs, why, dependencies
Every service + its role, grouped by tier. Read this alongside the diagram in §17.
### 16.1 Delivery tier (learner-facing)
| # | Service | Provider | Region | Purpose | Cost | Depends on | SLA in POC |
|---|---|---|---|---|---|---|---|
| D-1 | Learner browser | — | Global | Renders Rise → iframe → SPA | $0 | — | best-effort |
| D-2 | Rise 360 (Embed Block) | Articulate Cloud | US | Container for our iframe | $2,499/yr | Articulate uptime | vendor SLA |
| D-3 | React SPA | Cloudflare Pages | Edge PoPs | Cohort-code UI + prompt UI + response render + rate-limit countdown | $0 (free) | CF Pages | 99.9% |
| D-4 | Custom domain | Cloudflare DNS | Global | `ai101l.adbalabs.com` for iframe src | $0 | CF DNS + `adbalabs.com` zone | 100% |
### 16.2 API + auth tier
| # | Service | Provider | Region | Purpose | Cost | Depends on | SLA in POC |
|---|---|---|---|---|---|---|---|
| A-1 | Cloudflare Worker | Cloudflare | Edge PoPs | Router + JWT + rate limit + LLM proxy + telemetry writer + cron purge | $0 | CF Workers | 99.9% |
| A-2 | Worker Secrets | Cloudflare | Edge-replicated | Holds JWT + OIDC + WIF secrets | $0 | CF Workers | 99.9% |
| A-3 | Cohort code registry | KV (`cohort:*`) | Global | Validates learner cohort code | $0 | CF KV | eventual ≤60s |
| A-4 | JWT issuance | `jose` lib in Worker | Edge (per PoP) | Mints 24h HS256 tokens | $0 | secret A-2 | — |
### 16.3 LLM tier
| # | Service | Provider | Region | Purpose | Cost | Depends on | SLA in POC |
|---|---|---|---|---|---|---|---|
| L-1 | Vertex AI Gemini 2.5 Flash | GCP | northamerica-northeast1 | The tutor model | ~$8 Y1 total | GCP proj + WIF + `roles/aiplatform.user` | Google published |
| L-2 | Vertex AI Gemini 1.5 Flash | GCP | northamerica-northeast1 | Fallback if 2.5 not GA in region | same pricing | same as L-1 | same |
| L-3 | STS (token exchange) | GCP | Global | Exchanges Worker OIDC → GCP access token | $0 | WIF provider + IAM | 99.95% |
### 16.4 Storage + telemetry tier
| # | Service | Provider | Region | Purpose | Cost | Depends on | Retention |
|---|---|---|---|---|---|---|---|
| S-1 | KV `AI101L_STATE` | Cloudflare | Global (edge-replicated) | Rate-limit counters + prompt-hash + cohort registry | $0 | CF Workers | TTL per key |
| S-2 | D1 `ai101l_events` | Cloudflare | WNAM/ENAM | Plaintext prompt + response, quality review | $0 | CF Workers | 30d TTL (auto-purge cron) |
| S-3 | Analytics Engine | Cloudflare | Global | Cheap metric writes | $0 | CF Workers | 90 days |
| S-4 | BigQuery `ai101l.events` | GCP | northamerica-northeast1 | Aggregate learner-journey events for Looker | $0 (< 10GB free) | GCP proj + `roles/bigquery.dataEditor` | indefinite (no plaintext) |
| S-5 | Secret Manager | GCP | northamerica-northeast1 | Stores WIF config + backup SA JSON | $0 (< 6 secrets) | GCP proj | — |
### 16.5 Ops + governance tier
| # | Service | Provider | Purpose | Cost |
|---|---|---|---|---|
| O-1 | CF Access | Cloudflare | Email-gates architecture doc + admin URLs | $0 (< 50 users free) |
| O-2 | CF WAF | Cloudflare | Base bot/DDoS protection | $0 |
| O-3 | CF Turnstile (optional) | Cloudflare | Human check if cohort-code enumeration detected | $0 |
| O-4 | Looker Studio | GCP | Free dashboards on BigQuery | $0 |
| O-5 | wrangler CLI (fallback) | Local | Fallback deploy path if MCP breaks | $0 |
| O-6 | CF MCP servers × 5 | Cloudflare | Deploy/bindings/builds/docs/observability from Claude Code | $0 |
### 16.6 LMS integration tier (post-POC — W4/W5)
| # | Service | Provider | Purpose |
|---|---|---|---|
| M-1 | CITT Moodle | `learning.citt.ca` | SCORM 1.2 host for the 7 module packages |
| M-2 | CITT WooCommerce | `account.citt.ca` | Student enrolment + payment |
| M-3 | LTI 1.3 handshake | CITT + our Worker | Replaces cohort code (target: W4+) |
| M-4 | CCLP Hub (Cloud Generation) | `citt.hub.cloudgeneration.com` | Optional credit posting (TBD) |
---
## 17. Complete end-state component diagram — the final visual
This is the whole vision: every account, every service, every database, every hop, labelled and grouped. Reference for standing anything up. Colour convention matches §11 stencils.
```mermaid
flowchart TB
subgraph LR_TIER["📱 Learner Tier"]
BROWSER["Learner Browser
Chrome / Safari / Edge"]
end
subgraph CITT_TIER["🎓 CITT Tier — Post-POC (W4/W5)"]
subgraph CITT_LMS["Moodle · learning.citt.ca"]
COURSE["Course 1292
format-topcoll"]
SCORM_HOST["7 SCORM packages
M1..M7"]
end
subgraph CITT_WOO["WordPress · account.citt.ca"]
WOO["WooCommerce checkout
+ affiliate portal"]
end
CCLP["citt.hub.cloudgeneration.com
CCLP Achievements"]
end
subgraph ARTICULATE_TIER["🟥 Articulate 360 Cloud"]
RISE["Rise 360 lesson
+ Embed Block widget"]
REVIEW["Review 360
(W3+ SME workflow)"]
end
subgraph CF_TIER["🟧 Cloudflare — AdbaLabs Account"]
subgraph CF_DELIVERY["Delivery"]
PAGES_LAB["Pages: ai101l-lab
ai101l.adbalabs.com
React SPA (Vite)"]
PAGES_ARCH["Pages: ai101l-arch
arch.ai101l.adbalabs.com
THIS document"]
DNS_CF["Cloudflare DNS
(adbalabs.com zone)"]
end
subgraph CF_COMPUTE["Compute"]
WORKER["Worker: ai101l-worker
/api/v1/auth + /prompt
+ cron purge"]
WORKER_SECRETS["Worker Secrets
JWT / OIDC / WIF / KV admin"]
end
subgraph CF_STORAGE["Storage + Telemetry"]
KV_STATE["KV: AI101L_STATE
• cohort:*
• rl:h:* rl:d:*
• hash:*"]
D1_EV["D1: ai101l_events
plaintext 30d TTL
SQLite"]
AE_METRICS["Analytics Engine
ai101l_metrics"]
end
subgraph CF_GOVERNANCE["Governance"]
ACCESS["Access App
email-gate
(4 allowlisted)"]
WAF["WAF + Turnstile
(if needed)"]
end
subgraph CF_MCP["MCP (from Claude Code)"]
MCP_API["cloudflare-api"]
MCP_BIND["cloudflare-bindings"]
MCP_BUILDS["cloudflare-builds"]
MCP_DOCS["cloudflare-docs"]
MCP_OBS["cloudflare-observability"]
end
end
subgraph GCP_TIER["🟦 Google Cloud — AdbaLabs billing"]
subgraph GCP_PROJECT["Project: adbalabs-ai101l"]
subgraph GCP_MONTREAL["northamerica-northeast1 (Montreal)"]
VERTEX["Vertex AI
Gemini 2.5 Flash
(fallback: 1.5)"]
BQ_EV["BigQuery
dataset: ai101l
table: events"]
SM["Secret Manager
WIF config"]
LOOKER["Looker Studio
dashboards"]
end
subgraph GCP_IAM_BLOCK["IAM (global)"]
SA_WORKER["SA: ai101l-worker@
+ roles/aiplatform.user
+ roles/bigquery.dataEditor"]
WIF_POOL["Workload Identity Pool
cf-workers-pool"]
WIF_PROV["WIF Provider
cf-workers-oidc"]
STS["STS
token exchange"]
end
end
end
subgraph ADBALABS_TIER["🏢 AdbaLabs — Shared"]
DOMAIN["adbalabs.com
(CF zone)"]
GH["GitHub: adbadev1
(source of truth)"]
FOUNDER["mohammed@adbalabs.com
(Owner / DPO / on-call)"]
end
%% Data plane
BROWSER -->|HTTPS W5+| COURSE
COURSE -->|SCORM playback W5+| SCORM_HOST
SCORM_HOST -->|embed| RISE
BROWSER -->|HTTPS POC-direct or via LMS| RISE
RISE -->|iframe src=
ai101l.adbalabs.com| PAGES_LAB
PAGES_LAB -->|fetch /api/v1/*| WORKER
WORKER -->|GET cohort:code| KV_STATE
WORKER -->|INCR rl:*| KV_STATE
WORKER -->|SET hash:*| KV_STATE
WORKER -->|INSERT/UPDATE events| D1_EV
WORKER -->|metric write| AE_METRICS
WORKER -->|OIDC JWT| STS
STS -->|verify| WIF_PROV
WIF_PROV -->|trust chain| WIF_POOL
WIF_POOL -->|impersonate| SA_WORKER
STS -->|federated access token| WORKER
WORKER -->|generateContent
Bearer token| VERTEX
WORKER -->|streaming insert
Bearer token| BQ_EV
VERTEX -.->|IAM checks| SA_WORKER
BQ_EV -.->|IAM checks| SA_WORKER
LOOKER -->|SQL| BQ_EV
%% Governance
PAGES_ARCH -.->|behind| ACCESS
ACCESS -.->|allowlist check| FOUNDER
PAGES_LAB -.->|behind| WAF
WORKER -.->|reads| WORKER_SECRETS
WORKER -.->|reads WIF cfg| SM
%% Enrollment (post-POC)
WOO -.->|purchase to provision seat
W4/W5| COURSE
SCORM_HOST -.->|completion if wired| CCLP
%% DNS
PAGES_LAB -.->|CNAME| DNS_CF
PAGES_ARCH -.->|CNAME| DNS_CF
DNS_CF -.->|owns zone| DOMAIN
%% Build + ops
MCP_API -->|deploy Pages/Workers| CF_DELIVERY
MCP_API -->|deploy Pages/Workers| CF_COMPUTE
MCP_BIND -->|manage bindings| CF_STORAGE
MCP_OBS -->|read logs/metrics| WORKER
MCP_BUILDS -->|monitor builds| PAGES_LAB
GH -.->|source + infra-as-code| CF_COMPUTE
GH -.->|source + infra-as-code| CF_DELIVERY
FOUNDER -.->|Owner| GCP_PROJECT
FOUNDER -.->|admin| CF_TIER
FOUNDER -.->|admin| ARTICULATE_TIER
classDef gcp fill:#4285F4,color:#fff,stroke:#1a73e8
classDef cf fill:#F6821F,color:#fff,stroke:#c25100
classDef articulate fill:#EF3F49,color:#fff,stroke:#b02a32
classDef rise fill:#0EA5A5,color:#fff,stroke:#087272
classDef lms fill:#0f6cbf,color:#fff,stroke:#0a4d8f
classDef learner fill:#e8f0fe,color:#202124,stroke:#4285F4
classDef adba fill:#1e293b,color:#fff,stroke:#334155
class VERTEX,BQ_EV,SM,LOOKER,SA_WORKER,WIF_POOL,WIF_PROV,STS,GCP_PROJECT,GCP_MONTREAL,GCP_IAM_BLOCK gcp
class PAGES_LAB,PAGES_ARCH,DNS_CF,WORKER,WORKER_SECRETS,KV_STATE,D1_EV,AE_METRICS,ACCESS,WAF,MCP_API,MCP_BIND,MCP_BUILDS,MCP_DOCS,MCP_OBS cf
class RISE,REVIEW rise
class COURSE,SCORM_HOST,WOO,CCLP lms
class BROWSER learner
class DOMAIN,GH,FOUNDER adba
```
**How to read this diagram:**
- **Blue** boxes = Google Cloud (Montreal where region-scoped).
- **Orange** boxes = Cloudflare (edge or global).
- **Teal** = Rise 360; **coral** = wider Articulate 360 tier.
- **Moodle-blue** = CITT-owned surfaces (only active post-POC).
- **Slate-grey** = AdbaLabs shared infrastructure.
- **Solid arrows** = runtime request/response (data plane).
- **Dotted arrows** = IAM checks, trust chains, config reads, and build/ops flows (control plane).
**Verification lens.** A change to any component must be traceable in this diagram: if you can't draw the box, don't build the service. If you can't draw the arrow, don't wire the call. Use this as the pre-flight for every future PR that touches AI101L infra.
Loading architecture doc…