AcademyKit + QuestKit — Architecture Plan

Date: 2026-05-20 Goal: Two self-pluggable, multi-site-ready modules — one for email-gated learning, one for gamified social-share-to-earn-books.


The Two Systems

1. AcademyKit — Email-Login Learning Academy

A drop-in module that gates learning content behind email auth. Users log in once via magic link / OTP, then track progress, earn badges, and unlock books as they advance.

Pluggable interface (same pattern as emailGate, pushNotify):

window.AppCoreConfig = {
  academy: {
    enabled:    true,
    workerUrl:  'https://academy-gate.techiediaries.workers.dev',
    siteId:     'ahmedbouchefra2',
    loginMode:  'otp',          // 'otp' | 'magic-link'
    redirectUrl: '/academy/',
    strings: {                  // Arabic overrides
      loginTitle: 'أدخل بريدك للمتابعة',
      otpTitle:   'أدخل الرمز المُرسَل إليك',
    }
  }
}

Frontend module — assets/js/academy.js:

Backend Worker — academy-gate (BackendKit createHandler pattern):

POST /send-otp    { siteId, email }           → sends 6-digit OTP via Resend API
POST /verify-otp  { siteId, email, otp }      → validates, issues JWT (jose), stores session in KV
GET  /me          + Authorization: Bearer JWT  → returns { email, level, badges, progress }
POST /progress    { lessonId }                → marks lesson complete, checks badge triggers
GET  /badges      → list earned badges
POST /redeem      { bookId }                  → unlock book if level requirement met

Data files:

Level system:


2. QuestKit — Gamified Social Actions → Earn Books

Users complete social quests (follow, share, subscribe, install) to earn points, then redeem points for free books from the library.

Pluggable interface:

window.AppCoreConfig = {
  questGate: {
    enabled:   true,
    workerUrl: 'https://quest-gate.techiediaries.workers.dev',
    siteId:    'ahmedbouchefra2',
    currency:  'نقطة',    // Arabic "point"
    strings: {
      title:   'اكسب الكتب مجاناً',
      balance: 'رصيدك',
    }
  }
}

Quest types (honor-based by default, verifiable ones noted):

Quest Points Verification
Subscribe to newsletter 50 Hooks into emailGate — confirmed by Worker
Subscribe to push notifications 50 Hooks into pushNotify — Notification.permission === 'granted'
Install as PWA 30 display-mode: standalone check
Follow on X/Twitter 40 Honor + manual admin verify option
Follow on LinkedIn 40 Honor
Share a post (generates unique link) 60 Worker tracks link clicks
Refer a friend (they subscribe) 100 Worker checks referral code on signup
Write a review (URL proof) 80 URL submitted → admin queue
Complete an academy lesson 20/each Academy Worker cross-call

Frontend module — assets/js/quest-gate.js:

Backend Worker — quest-gate (BackendKit createHandler):

POST /identify     { siteId, email }          → register user (email is the identifier)
GET  /quests       { siteId, email }          → list quests + completion status
POST /claim        { siteId, email, questId } → award points (with abuse guard: once per quest)
GET  /balance      { siteId, email }          → current points
GET  /rewards      { siteId }                 → books available for redemption + costs
POST /redeem       { siteId, email, bookId }  → redeem points for book, return access URL
POST /share-click  { ref }                    → track click on share link
GET  /leaderboard  { siteId }                 → top 10 by points

Data files:


Architecture Pillars

Self-Pluggable Pattern

Both modules follow the exact same pattern as existing modules (emailGate, pushNotify):

  1. One JS file — drop in before app-core.js loads
  2. One AppCoreConfig key — all config in one place
  3. One Cloudflare Worker — siteId-namespaced KV, same Worker serves multiple sites
  4. No framework dependency — vanilla JS, works on Jekyll, Next.js, anything
  5. Event-driven integration — emits and listens to CustomEvent on document

BackendKit Integration — All Three Layers

Both Workers are full BackendKit apps, not bare Cloudflare Workers. BackendKit contributes at every layer:

Layer 1: Compute (ComputeRouter) — deployment portability

// academy-worker/backendkit.config.js
compute: {
  providers: [
    { type: 'cloudflare',    priority: 1 },   // production
    { type: 'supabase-edge', priority: 2 },   // auto-fallback on rate_limited / down
    { type: 'local',         priority: 99 },  // dev (node-server.js)
  ]
}

Same worker code runs on Cloudflare Worker, Supabase Edge, or Node locally — zero changes. BackendKit’s rotator.js handles providerSignal: 'rate_limited' failover. The academy-worker is a BackendKit app in exactly the same sense as the bookmarks demo.

Layer 2: Data (DataRouter) — storage abstraction, no raw KV

Instead of hardcoded env.KV.get/put, route functions call dataRouter.query(). Both adapters already exist (adapters/data/pocketbase.js, adapters/data/supabase.js):

// academy-worker/backendkit.config.js
data: {
  strategy: 'read-fallback',
  providers: [
    { type: 'pocketbase', url: process.env.PB_URL,       priority: 1 },
    { type: 'supabase',   url: process.env.SUPABASE_URL, priority: 2 },
  ]
}

// route function — zero provider coupling
async function saveProgress({ body }, { dataRouter }) {
  return dataRouter.query({
    collection: 'academy_progress',
    action:     'create',
    body:       { email: body.email, siteId: body.siteId, lessonId: body.lessonId }
  })
}

Swap PocketBase for Supabase or Turso — the route doesn’t change. Data schema lives in PocketBase collections (academy_sessions, academy_progress, academy_badges, quest_claims, quest_balance) — real schema, not KV string hacks.

Layer 3: Auth (authAdapter) — already built and tested (12 tests)

BackendKit Phase 3 delivered createJwtAuthAdapter({ secret, expiresIn, dataRouter }) using jose. Academy uses it directly — no reimplementation:

// loadConfig wires it from config:
// auth: { type: 'jwt', secret: process.env.JWT_SECRET, expiresIn: '7d' }

// verify-otp route:
const token = await authAdapter.sign({ sub: email, siteId, level: 1 })

// protected routes (GET /me, POST /progress):
const user = await authAdapter.verify(req.headers.get('Authorization')?.slice(7))
if (!user) return { status: 401, data: { error: 'غير مصرح' } }

QuestKit identifies users by email only (no password) — no authAdapter.login() needed, just authAdapter.sign() after OTP verification.

Layer 4: HTTP (createHandler) — runtime-portable routing

// academy-worker/index.js
import { loadConfig }    from 'backendkit/config-loader'
import { createHandler } from 'backendkit/handler'
import { serve }         from 'backendkit/node-server'
import * as routes       from './routes/index.js'

const { dataRouter, authAdapter } = await loadConfig('./backendkit.config.js')
const ctx = { dataRouter, authAdapter }

const handler = createHandler([
  ['POST', /^\/send-otp$/,   (r) => routes.sendOtp(r, ctx)],
  ['POST', /^\/verify-otp$/, (r) => routes.verifyOtp(r, ctx)],
  ['GET',  /^\/me$/,         (r) => routes.getProfile(r, ctx)],
  ['POST', /^\/progress$/,   (r) => routes.saveProgress(r, ctx)],
  ['GET',  /^\/badges$/,     (r) => routes.getBadges(r, ctx)],
])

export default { fetch: handler }    // Cloudflare Worker
// serve(handler, { port: 3001 })    // Node dev — uncomment locally

Corrected architecture

academy-worker/           quest-worker/
├─ index.js               ├─ index.js
│   createHandler([...])  │   createHandler([...])    ← BackendKit HTTP
├─ routes/                ├─ routes/
│   sendOtp, verifyOtp,   │   identify, claim,        ← thin fns, only domain logic
│   progress, badges,     │   balance, redeem,        
│   getProfile            │   shareClick, leaderboard 
└─ backendkit.config.js   └─ backendkit.config.js
    compute: CF→SBedge→local  compute: CF→SBedge→local  ← BackendKit Compute
    data: PB→Supabase         data: PB→Supabase          ← BackendKit Data
    auth: jwt/7d              (no auth — email=identity)  ← BackendKit Auth

New things written per worker: OTP send (Resend API call) + domain-specific route bodies. Everything else: BackendKit.

Data Schema (PocketBase collections, multi-site via siteId field)

No KV string hacks — proper collections via BackendKit DataRouter:

Collection          Fields
──────────────────────────────────────────────────────────────
academy_users       siteId, email, level, xp, createdAt
academy_progress    siteId, email, lessonId, completedAt
academy_badges      siteId, email, badgeId, earnedAt
academy_sessions    siteId, email, token (JWT), expiresAt
quest_claims        siteId, email, questId, claimedAt
quest_balance       siteId, email, points
quest_redeemed      siteId, email, bookId, redeemedAt, accessUrl
quest_share_clicks  ref (hash), questId, email, clickedAt, ip

Multi-site isolation: every record has a siteId field. Same PocketBase instance serves ahmedbouchefra2, webtutpro, etc. — dataRouter filters by siteId on every query. No cross-site data leakage.

Cross-module event bus

// academy.js emits after lesson complete:
document.dispatchEvent(new CustomEvent('academy:lesson-complete', { detail: { lessonId } }))

// quest-gate.js listens:
document.addEventListener('academy:lesson-complete', ({ detail }) => claimQuest('academy-lesson'))

// emailGate.js already dispatches emailGate:subscribed — quest-gate listens
document.addEventListener('emailGate:subscribed', () => claimQuest('newsletter'))

Build Phases

AcademyKit Phases

Phase Deliverable
A1 academy-worker: /send-otp + /verify-otp + KV session (Resend + jose)
A2 academy.js: login UI (bottom-sheet, OTP input), session check, logout
A3 _data/academy_lessons.yml + /progress endpoint + Academy.markComplete()
A4 _data/academy_badges.yml + badge triggers in Worker + badge shelf in UI
A5 Book unlock by academy level — cross-check resources.yml + academy level
A6 cli/academy.js + MCP tools (academy_status, academy_lessons, academy_badges)
A7 ab admin integration — view enrolled users, progress, badge breakdown

QuestKit Phases

Phase Deliverable
Q1 _data/quests.yml + _data/quest_rewards.yml — data model + CLI viewer
Q2 quest-worker: /identify + /claim + /balance + /rewards + /redeem
Q3 quest-gate.js: quest panel UI, claim buttons, balance display, redemption
Q4 Share link system: Worker generates ?ref=<hash> + /share-click tracker
Q5 Referral system: ?ref=<hash> attribution on signup → 100 pts to referrer
Q6 Leaderboard: /leaderboard endpoint + optional embed widget
Q7 cli/quests.js + MCP tools (quests_list, quests_balance, quests_redeem)

Integration Phase (both complete)

Phase Deliverable
I1 Cross-module events: emailGate → quest, pushNotify → quest, academy → quest
I2 Library page upgrade: show quest point cost per book alongside gate options
I3 npm publish: @techiediaries/academy-kit + @techiediaries/quest-kit

Resend API (email OTP)

Free tier: 3,000 emails/month. No credit card for dev.

// Inside academy-worker
await fetch('https://api.resend.com/emails', {
  method: 'POST',
  headers: { 'Authorization': `Bearer ${RESEND_API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    from:    '[email protected]',
    to:      email,
    subject: 'رمز الدخول إلى أكاديمية أحمد',
    html:    `<p>رمزك: <strong>${otp}</strong> — صالح 10 دقائق</p>`,
  })
})

What already exists and can be reused

Existing Reused by
emailGate.js email flow pattern Academy login UI structure
resource_gate.js bottom-sheet component Quest panel + Academy gate UI
book-gate Worker KV patterns Academy + Quest KV structure
BackendKit createHandler Both Workers
BackendKit JWT auth (Phase 3, jose) Academy session tokens
_data/library.yml book entries Quest rewards catalog
_data/resources.yml Academy book unlock targets
AppCoreConfig + app-core.js loader Both module configs
cli/hub.js + mcp-server.mjs Academy + Quest CLI/MCP tools