Skip to content

earth-app/cloud

cloud

The Automaton behind The Earth App

A sophisticated Cloudflare Workers-based microservice that powers The Earth App's AI-driven content generation, recommendations, events, realtime notifications, and user progression systems. Built with Hono.js, this service orchestrates multiple AI models, manages distributed caching, and exposes a comprehensive REST + WebSocket API for activity discovery, article curation, quest tracking, image submissions, and personalized recommendations.

Table of Contents

Architecture Overview

The Cloud service is a serverless application running on Cloudflare Workers with three primary runtime surfaces: REST APIs, WebSocket notification channels, and scheduled automation.

┌─────────────────────────────────────────────────────────────────────┐
│                         Hono.js Edge Router                         │
│  ┌────────────────┐  ┌────────────────┐  ┌──────────────────────┐   │
│  │ Middleware     │  │ API /v1        │  │ API /ws              │   │
│  │ - Security     │  │ - Activities   │  │ - Ticket issuance    │   │
│  │ - CORS         │  │ - Articles     │  │ - Live notifications │   │
│  │ - Logger       │  │ - Events       │  │ - Admin push         │   │
│  │ - HTTP cache   │  │ - Quests       │  │                      │   │
│  └────────────────┘  │ - Badges/Points│  └──────────────────────┘   │
│                      │ - Journeys     │                             │
│                      └────────────────┘                             │
│                    Scheduled Worker (cron triggers)                 │
└─────────────────────────────────────────────────────────────────────┘
               │                 │                 │
               ▼                 ▼                 ▼
     ┌────────────────┐  ┌───────────────┐  ┌────────────────────┐
     │ Cloudflare AI  │  │ KV + Cache KV │  │ R2 + Images Binding│
     └────────────────┘  └───────────────┘  └────────────────────┘
                         ┌──────────────────────────────────────────┐
                         │ Durable Objects                          │
                         │ - LiveNotifier (WebSocket fan-out)       │
                         │ - UserTimer (user timer actions)         │
                         └──────────────────────────────────────────┘

Request Flow

  1. Incoming request -> middleware stack (headers, CORS, logging, cache)
  2. Authentication ->
    • /v1/*: Authorization: Bearer {ADMIN_API_KEY}
    • /ws/notify: admin key
    • /ws/users/:id/ticket: user session token validation with Mantle
  3. Cache lookup -> deterministic key checks in CACHE namespace
  4. Business logic -> AI invocation, ranking, validation, KV/R2 reads+writes
  5. Response -> JSON or binary response with custom headers

Mantle2 Integration

This worker is the Cloudflare backend that the Drupal 11 earth-app/mantle2 module calls through CloudHelper::sendRequest(...). The PHP side depends on the shape and status codes of the worker's /v1 endpoints for quest progress, impact points, events, thumbnails, quizzes, profile photos, and notifications.

  • Quest progress is shared across both systems through QuestStep, QuestProgressEntry, and QuestData-style payloads, so validation changes should be checked against the Mantle2 quest helpers before they land.
  • Event and article flows are also coupled to Mantle2's data model. If you adjust event creation, quiz persistence, or submission payloads, verify the corresponding consumer helpers and schema expectations in the Drupal module.
  • WebSocket tickets, timers, and image moderation stay worker-owned, but Mantle2 still relies on their request and response contracts when it proxies or displays those features.

Technology Stack

Core Dependencies

  • hono (^4.12.10): edge-optimized HTTP framework and middleware routing.
  • @earth-app/ocean (^1.0.4): Kotlin/WASM library for article/event search and activity recommendations.
  • @earth-app/moho (^1.0.1): calendar/event data source used for scheduled event generation.
  • pako (^2.1.0): compression support (nodejs_zlib).
  • exifreader (^4.37.0): EXIF parsing for event image metadata workflows.
  • music-metadata (^11.12.3): audio metadata utilities for media processing.
  • @cloudflare/workerd-darwin-arm64 (^1.20260405.1): local runtime binary support while developing on Apple Silicon.

Type Strategy

  • Runtime/Worker types are generated with Wrangler into src/worker-configuration.d.ts.
  • @cloudflare/workers-types has been removed from dependencies and tsconfig.json.

Development Tools

  • Wrangler (^4.80.0): local dev, deploy, and type generation
  • Vitest (^4.1.2) + @cloudflare/vitest-pool-workers (^0.14.1): worker-native tests
  • Prettier (^3.8.1): formatting with Husky + lint-staged
  • Bun: package/runtime tooling

Infrastructure & Bindings

The service integrates with Cloudflare services via Worker bindings:

type Bindings = {
	R2: R2Bucket;
	AI: Ai;
	KV: KVNamespace;
	CACHE: KVNamespace;
	ASSETS: Fetcher;
	IMAGES: ImagesBinding;
	NOTIFIER: DurableObjectNamespace;
	TIMER: DurableObjectNamespace;

	ADMIN_API_KEY: string;
	NCBI_API_KEY: string;
	MANTLE_URL: string;
	MAPS_API_KEY: string;
	ENCRYPTION_KEY: string;
};

Cloudflare KV Namespaces

  • CACHE (c4a1aaf2a5fc4be98b91df2d0fc0faab): ephemeral cache (12-hour default TTL)

    • Activity metadata and synonyms
    • Article/event recommendation results
    • Scoring results
    • Leaderboards and profile photo responses
  • KV (322faefd5628471cb7cea08cf041804a): persistent and semi-persistent app state

    • Journey streaks and activity completion logs
    • Badge progress + grant metadata
    • Impact points history
    • Quest progress/history metadata
    • Event submission indices and score metadata

R2 Bucket

  • Bucket: earth-app (prod)
  • Primary objects:
    • users/{id}/profile.png and resized variants
    • events/{eventId}/thumbnail.webp
    • events/{eventId}/submissions/{userId}_{submissionId}.webp (encrypted)
    • users/{id}/quests/{questId}/... binary quest evidence (compressed + encrypted)

Durable Objects

  • LiveNotifier (NOTIFIER):

    • Issues one-time WebSocket tickets
    • Enforces one-time ticket consumption with transactional storage
    • Fans out pushed notifications to connected sockets
  • UserTimer (TIMER):

    • Tracks per-user timer actions (start/stop)
    • Applies duration-based progress updates (e.g., reading-time trackers)

External Services

  • NCBI PubMed APIs (article search)
  • Iconify API (activity icon resolution)
  • Dictionary API (activity synonyms)
  • Google Places + Geocoding APIs (event thumbnail lookup/reverse geocode)
  • Mantle backend API (MANTLE_URL) for persistence integration

Core Features

1. Dynamic Activity Generation

GET /v1/activity/:id generates and caches activity metadata:

  • AI-generated 200+ character descriptions with retry + validation
  • Activity type classification
  • Synonym enrichment
  • Icon lookup from preferred icon sets

2. Scientific Article Curation + Quiz Generation

Automated article pipeline:

  1. Generate a topic
  2. Search source articles via Ocean scrapers
  3. Rank with semantic reranker
  4. Build polished title + summary
  5. Generate 2-5 quiz questions
  6. Publish to Mantle and cache quiz payload

Scheduled article creation now generates two pieces per run (best-ranked and worst-ranked) to improve diversity.

3. Multi-Domain Recommendations

  • Articles: POST /v1/users/recommend_articles
  • Similar Articles: POST /v1/articles/recommend_similar_articles
  • Activities: POST /v1/users/recommend_activities
  • Events: POST /v1/users/recommend_events
  • Similar Events: POST /v1/events/recommend_similar_events

All recommendation paths use AI ranking with deterministic cache keys and fallback behavior.

4. User Journeys + Leaderboards

Tracks streaks for article, prompt, and event journeys with:

  • 24-hour increment cooldown
  • 2-day rolling TTL renewal
  • Cached top leaderboard snapshots
  • Rank lookup endpoint
  • Separate permanent activity completion logs

5. Badges, Impact Points, and Timers

User progression includes:

  • Rule-based badge tracking and granting
  • Manual admin operations (grant/revoke/reset)
  • Impact point accounting with history
  • Timer-driven tracker updates through Durable Object actions

6. Quest Engine (Multimodal)

Quest steps support image, audio, article quiz, and structured interactions:

  • Binary quest artifacts are compressed + encrypted before R2 storage
  • Per-step delay windows and alternate step handling
  • Completed quest archiving + retrieval
  • Quest progress enrichment with generated data URLs for retrieval APIs

7. Event Creation + Thumbnail Automation

Every 2 days, the worker generates events from Moho calendar data.

  • Birthday-style events are parsed for location extraction
  • Place photos are discovered with Google Places APIs
  • Thumbnails are converted to WebP, stored in R2, and exposed via metadata-rich endpoints
  • Event creation continues even when thumbnail generation fails (best-effort resilience)

8. Event Image Submissions + Scoring

Users can submit event images (data URL payloads), then retrieve scored results:

  • Image normalization/transforms via Images binding
  • Encrypted object storage in R2
  • Score + caption generation via AI rubric
  • Query endpoints for submission lookup, pagination, filtering, and deletion

9. Realtime WebSocket Notifications

WebSocket flow under /ws:

  1. Client requests a one-time ticket (/ws/users/:id/ticket)
  2. Ticket is validated and consumed on connect (/ws/users/:id/notifications?ticket=...)
  3. Backend/admin sends payloads through /ws/notify

Security details include no-store headers, masked ticket logging, and strict one-time semantics.

10. AI-Generated Profile Photos

PUT /v1/users/profile_photo/:id generates a profile image and asynchronously creates size variants (32, 128, original) with Images binding + R2 persistence.

API Reference

Auth Model

  • All /v1/* endpoints require Authorization: Bearer {ADMIN_API_KEY}.
  • /ws/notify also requires admin bearer auth.
  • WebSocket user channels use session-validated one-time tickets (not admin keys).

Root

Method Endpoint Description
GET / Health check (Woosh!)

Admin

Method Endpoint Description
POST /v1/admin/migrate-legacy-keys Migrates legacy KV key formats

Activities

Method Endpoint Description
GET /v1/activity/:id Generate/retrieve activity metadata
GET /v1/synonyms?word={word} Retrieve synonyms for naming/aliasing

Articles

Method Endpoint Description
GET /v1/articles/search?q={query} Search article sources
POST /v1/articles/recommend_similar_articles Similar article recommendations
POST /v1/articles/grade AI rubric score for article text
POST /v1/articles/quiz/create Generate and persist article quiz
GET /v1/articles/quiz?articleId={id} Fetch article quiz
POST /v1/articles/quiz/submit Submit user quiz answers
GET /v1/articles/quiz/score?userId={id}&articleId={id} Fetch saved quiz score

Prompts

Method Endpoint Description
POST /v1/prompts/grade AI rubric score for prompt text

User Recommendations + Profiles

Method Endpoint Description
POST /v1/users/recommend_activities Activity recommendations
POST /v1/users/recommend_articles Article recommendations by activities
POST /v1/users/recommend_events Event recommendations by activities
GET /v1/users/profile_photo/:id?size={32|128|1024} Retrieve profile photo variant
PUT /v1/users/profile_photo/:id Generate/replace profile photo
POST /v1/users/timer Timer Durable Object actions

Journeys

Method Endpoint Description
GET /v1/users/journey/:type/:id Get journey streak + rank
POST /v1/users/journey/:type/:id/increment Increment streak with cooldown logic
DELETE /v1/users/journey/:type/:id/delete Reset streak
GET /v1/users/journey/:type/leaderboard?limit={n} Get top leaderboard
GET /v1/users/journey/:type/:id/rank Get user rank
GET /v1/users/journey/activity/:id/count Count completed activities
POST /v1/users/journey/activity/:id?activity={name} Add completed activity

Badges

Method Endpoint Description
GET /v1/users/badges List badge catalog
GET /v1/users/badges/:id List user's badge states
GET /v1/users/badges/:id/:badge_id Get single badge state
POST /v1/users/badges/:id/track Track badge progress by tracker ID
POST /v1/users/badges/:id/:badge_id/progress Add progress for badge tracker
POST /v1/users/badges/:id/:badge_id/grant Manually grant one-time badge
DELETE /v1/users/badges/:id/:badge_id/revoke Revoke granted badge
DELETE /v1/users/badges/:id/:badge_id/reset Reset badge progress

Impact Points

Method Endpoint Description
GET /v1/users/impact_points/:id Get points + history
POST /v1/users/impact_points/:id/add Add points
POST /v1/users/impact_points/:id/remove Remove points
PUT /v1/users/impact_points/:id/set Set absolute points

Quests

Method Endpoint Description
GET /v1/users/quests List quest definitions
GET /v1/users/quests/:id Get quest definition
POST /v1/users/quests/progress/:user_id Start quest
PATCH /v1/users/quests/progress/:user_id Submit step response
GET /v1/users/quests/progress/:user_id Get active progress
GET /v1/users/quests/progress/:user_id/step/:step_index Get specific step progress
DELETE /v1/users/quests/progress/:user_id Reset active progress
GET /v1/users/quests/history/:user_id List completed quests
GET /v1/users/quests/history/:user_id/:quest_id Get completed quest payload

Events + Event Media

Method Endpoint Description
GET /v1/events/thumbnail/:id Get event thumbnail image
GET /v1/events/thumbnail/:id/metadata Get thumbnail author + size metadata
POST /v1/events/thumbnail/:id Upload custom thumbnail
POST /v1/events/thumbnail/:id/generate?name={event_name} Generate location-based thumbnail
DELETE /v1/events/thumbnail/:id Delete event thumbnail
POST /v1/events/recommend_similar_events Similar event recommendations
POST /v1/events/submit_image Submit event image
GET /v1/events/retrieve_image?... Retrieve image submission(s) with optional filters
DELETE /v1/events/delete_image?... Delete one or many image submissions

WebSocket Routes

Method Endpoint Description
POST /ws/notify Admin push payload to a channel
GET /ws/users/:id/ticket Issue one-time WebSocket ticket (session validated)
GET /ws/users/:id/notifications?ticket={uuid} Upgrade to user notification WebSocket

AI Models & Prompting

Model Selection Strategy

Use Case Model Rationale
Activity descriptions @cf/meta/llama-4-scout-17b-16e-instruct Rich descriptive generation
Activity tags @cf/meta/llama-3.1-8b-instruct-fp8 Fast structured tagging
Article topic generation @cf/meta/llama-3.2-3b-instruct Lightweight topic selection
Semantic ranking (articles/events) @cf/baai/bge-reranker-base Strong reranking quality
Article title + summary @cf/mistralai/mistral-small-3.1-24b-instruct Long-form summarization quality
Article quiz generation @cf/meta/llama-4-scout-17b-16e-instruct Reliable structured question output
Prompt generation @cf/openai/gpt-oss-120b Higher-order prompt reasoning
Profile photo generation @cf/bytedance/stable-diffusion-xl-lightning Fast image synthesis
Text embeddings for scoring @cf/baai/bge-m3 Semantic similarity scoring
Image captioning for scoring @cf/llava-hf/llava-1.5-7b-hf Visual-to-text interpretation
Image classification @cf/microsoft/resnet-50 Label confidence checks
Object detection @cf/facebook/detr-resnet-50 Object-level validation
Audio transcription @cf/openai/whisper-large-v3-turbo Quest audio validation

Output Sanitization

src/util/ai.ts includes a centralized sanitation/validation pipeline:

  1. Remove markdown artifacts and wrappers
  2. Remove common AI prefixes and formatting noise
  3. Normalize whitespace and punctuation
  4. Apply content-type-specific cleanup (description, title, topic, tags, question)
  5. Enforce strict validators per domain object

Caching Strategy

Multi-Tier Architecture

┌──────────────────────────────────────┐
│ HTTP Cache Middleware               │
│ Scope: /v1/*                        │
│ Cache-Control: public, max-age=60   │
└──────────────────────────────────────┘
              │
              ▼
┌──────────────────────────────────────┐
│ KV Cache (CACHE namespace)          │
│ Default TTL: 12h                    │
│ Includes Uint8Array custom serializer│
└──────────────────────────────────────┘
              │
              ▼
┌──────────────────────────────────────┐
│ Source of Truth                      │
│ AI + External APIs + KV/R2           │
└──────────────────────────────────────┘

Key Patterns

// Activities and synonyms
`cache:activity_data:${id}``cache:synonyms:${word.toLowerCase()}`
// Recommendations
`cache:recommended_articles:${activitiesHash}:${poolHash}:${limit}``cache:similar_articles:${articleId}:${poolHash}:${limit}``cache:recommended_events:${activitiesHash}:${poolHash}:${limit}``cache:similar_events:${eventId}:${poolHash}:${limit}`
// Scoring and profile
`cache:article_score:${id}``cache:prompt_score:${id}``user:profile_photo:${id}:${size}`
// Journey + leaderboard
`journey:${type}:${id}``journey:activities:${id}``leaderboard:${type}`;

TTL Notes

  • Default cache TTL: 12 hours
  • Leaderboard cache TTL: 4 hours
  • Article score cache: 14 days
  • Prompt score cache: 2 days
  • Article quiz cache: about 14 days
  • Journey streak keys in KV: 2-day TTL, renewed by activity
  • /ws/* routes explicitly use no-store semantics

Serialization Edge Cases

Custom reviver/replacer handles binary payloads:

JSON.stringify(value, (_, val) =>
	val instanceof Uint8Array ? { __type: 'Uint8Array', data: Array.from(val) } : val
);

JSON.parse(result, (_, val) => (val?.__type === 'Uint8Array' ? new Uint8Array(val.data) : val));

Hourly: Leaderboard Cache

  • Refreshes top rankings for article, prompt, and event journeys.

Every 12 Minutes: Prompt Generation

  1. Generate one validated question prompt
  2. Publish to Mantle (/v2/prompts)

Every 24 Minutes: Article Pair Generation

  1. Generate topic and tags
  2. Search + rank source articles
  3. Create and post two article variants (best-ranked and worst-ranked)
  4. Generate and attach quizzes

Every 2 Days: Event Generation

  1. Load upcoming events from Moho data
  2. Create event payloads and post to Mantle
  3. Attempt birthday-location thumbnail generation when applicable
  4. Continue processing even when individual event creation fails

Development

Prerequisites

  • Bun (>= 1.0.0)
  • Wrangler (installed via project dependencies)
  • Cloudflare account with Workers, KV, R2, AI, Images, and Durable Objects enabled

Local Development

# Install dependencies
bun install

# Start local dev server (port 9898, scheduled testing enabled)
bun run dev

# Test endpoint
curl http://localhost:9898/v1/activity/hiking \
  -H "Authorization: Bearer YOUR_DEV_API_KEY"

Testing

# Run worker tests
bunx vitest

Regenerate Worker Types

# Regenerate runtime types used by TypeScript
bunx wrangler types

Debugging AI Calls

console.log('AI Request:', { model, messages });
const response = await ai.run(model, params);
console.log('AI Response:', response);

Common issues:

  • Empty response: model availability or payload mismatch
  • Validation failure: inspect raw output before sanitation
  • Timeout: reduce context size or choose a lighter model

Deployment

Production Deployment

# Deploy to Cloudflare Workers
bun run deploy

Deployment flow:

  1. Build/minify worker bundle
  2. Upload worker + bindings config
  3. Apply route + cron configuration from wrangler.jsonc
  4. Activate on cloud.earth-app.com

License

All Earth App components are available open-source. This repository is licensed under the Apache 2.0 License.

The Earth App (c) 2025

Contributors

Maintained by the Earth App development team.

For questions or support, contact: support@earth-app.com

About

🤖 The Automaton behind The Earth App

Topics

Resources

License

Code of conduct

Security policy

Stars

Watchers

Forks

Releases

No releases published

Sponsor this project

Contributors