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.
- Architecture Overview
- Technology Stack
- Infrastructure & Bindings
- Core Features
- API Reference
- AI Models & Prompting
- Caching Strategy
- Scheduled Tasks
- Development
- Deployment
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) │
└──────────────────────────────────────────┘- Incoming request -> middleware stack (headers, CORS, logging, cache)
- Authentication ->
/v1/*:Authorization: Bearer {ADMIN_API_KEY}/ws/notify: admin key/ws/users/:id/ticket: user session token validation with Mantle
- Cache lookup -> deterministic key checks in
CACHEnamespace - Business logic -> AI invocation, ranking, validation, KV/R2 reads+writes
- Response -> JSON or binary response with custom headers
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, andQuestData-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.
- 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.
- Runtime/Worker types are generated with Wrangler into
src/worker-configuration.d.ts. @cloudflare/workers-typeshas been removed from dependencies andtsconfig.json.
- 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
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;
};-
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
- Bucket:
earth-app(prod) - Primary objects:
users/{id}/profile.pngand resized variantsevents/{eventId}/thumbnail.webpevents/{eventId}/submissions/{userId}_{submissionId}.webp(encrypted)users/{id}/quests/{questId}/...binary quest evidence (compressed + encrypted)
-
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)
- Tracks per-user timer actions (
- 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
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
Automated article pipeline:
- Generate a topic
- Search source articles via Ocean scrapers
- Rank with semantic reranker
- Build polished title + summary
- Generate 2-5 quiz questions
- Publish to Mantle and cache quiz payload
Scheduled article creation now generates two pieces per run (best-ranked and worst-ranked) to improve diversity.
- 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.
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
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
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
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)
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
WebSocket flow under /ws:
- Client requests a one-time ticket (
/ws/users/:id/ticket) - Ticket is validated and consumed on connect (
/ws/users/:id/notifications?ticket=...) - Backend/admin sends payloads through
/ws/notify
Security details include no-store headers, masked ticket logging, and strict one-time semantics.
PUT /v1/users/profile_photo/:id generates a profile image and asynchronously creates
size variants (32, 128, original) with Images binding + R2 persistence.
- All
/v1/*endpoints requireAuthorization: Bearer {ADMIN_API_KEY}. /ws/notifyalso requires admin bearer auth.- WebSocket user channels use session-validated one-time tickets (not admin keys).
| Method | Endpoint | Description |
|---|---|---|
| GET | / |
Health check (Woosh!) |
| Method | Endpoint | Description |
|---|---|---|
| POST | /v1/admin/migrate-legacy-keys |
Migrates legacy KV key formats |
| Method | Endpoint | Description |
|---|---|---|
| GET | /v1/activity/:id |
Generate/retrieve activity metadata |
| GET | /v1/synonyms?word={word} |
Retrieve synonyms for naming/aliasing |
| 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 |
| Method | Endpoint | Description |
|---|---|---|
| POST | /v1/prompts/grade |
AI rubric score for prompt text |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
src/util/ai.ts includes a centralized sanitation/validation pipeline:
- Remove markdown artifacts and wrappers
- Remove common AI prefixes and formatting noise
- Normalize whitespace and punctuation
- Apply content-type-specific cleanup (
description,title,topic,tags,question) - Enforce strict validators per domain object
┌──────────────────────────────────────┐
│ 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 │
└──────────────────────────────────────┘// 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}`;- 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 useno-storesemantics
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));- Refreshes top rankings for
article,prompt, andeventjourneys.
- Generate one validated question prompt
- Publish to Mantle (
/v2/prompts)
- Generate topic and tags
- Search + rank source articles
- Create and post two article variants (best-ranked and worst-ranked)
- Generate and attach quizzes
- Load upcoming events from Moho data
- Create event payloads and post to Mantle
- Attempt birthday-location thumbnail generation when applicable
- Continue processing even when individual event creation fails
- Bun (>= 1.0.0)
- Wrangler (installed via project dependencies)
- Cloudflare account with Workers, KV, R2, AI, Images, and Durable Objects enabled
# 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"# Run worker tests
bunx vitest# Regenerate runtime types used by TypeScript
bunx wrangler typesconsole.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
# Deploy to Cloudflare Workers
bun run deployDeployment flow:
- Build/minify worker bundle
- Upload worker + bindings config
- Apply route + cron configuration from
wrangler.jsonc - Activate on
cloud.earth-app.com
All Earth App components are available open-source. This repository is licensed under the Apache 2.0 License.
The Earth App (c) 2025
Maintained by the Earth App development team.
For questions or support, contact: support@earth-app.com