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:

typeBindings={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

The tables below cover the main surface. src/app.ts is the source of truth for the full route list.

Auth Model

  • All /v1/* endpoints require Authorization: Bearer {ADMIN_API_KEY}.
  • User and content ids accept either shape: the legacy numeric id, or the 32-hex public id mantle2 now issues. Cloud resolves a public id to the numeric one its KV is keyed on and caches the pair.
  • /ws/notify also requires admin bearer auth.
  • WebSocket user channels use session-validated one-time tickets (not admin keys).

Root

MethodEndpointDescription
GET/Health check (Woosh!)

Admin

MethodEndpointDescription
POST/v1/admin/migrate-legacy-keysMigrates legacy KV key formats

Activities

MethodEndpointDescription
GET/v1/activity/:idGenerate/retrieve activity metadata
GET/v1/synonyms?word={word}Retrieve synonyms for naming/aliasing

Articles

MethodEndpointDescription
GET/v1/articles/search?q={query}Search article sources
POST/v1/articles/recommend_similar_articlesSimilar article recommendations
POST/v1/articles/gradeAI rubric score for article text
POST/v1/articles/quiz/createGenerate and persist article quiz
GET/v1/articles/quiz?articleId={id}Fetch article quiz
POST/v1/articles/quiz/submitSubmit user quiz answers
GET/v1/articles/quiz/score?userId={id}&articleId={id}Fetch saved quiz score

Prompts

MethodEndpointDescription
POST/v1/prompts/gradeAI rubric score for prompt text

User Recommendations + Profiles

MethodEndpointDescription
POST/v1/users/recommend_activitiesActivity recommendations
POST/v1/users/recommend_articlesArticle recommendations by activities
POST/v1/users/recommend_eventsEvent recommendations by activities
GET/v1/users/profile_photo/:id?size={32|128|1024}Retrieve profile photo variant
PUT/v1/users/profile_photo/:idGenerate/replace profile photo
POST/v1/users/timerTimer Durable Object actions

Journeys

MethodEndpointDescription
GET/v1/users/journey/:type/:idGet journey streak + rank
POST/v1/users/journey/:type/:id/incrementIncrement streak with cooldown logic
DELETE/v1/users/journey/:type/:id/deleteReset streak
GET/v1/users/journey/:type/leaderboard?limit={n}Get top leaderboard
GET/v1/users/journey/:type/:id/rankGet user rank
GET/v1/users/journey/activity/:id/countCount completed activities
POST/v1/users/journey/activity/:id?activity={name}Add completed activity

Badges

MethodEndpointDescription
GET/v1/users/badgesList badge catalog
GET/v1/users/badges/:idList user's badge states
GET/v1/users/badges/:id/:badge_idGet single badge state
POST/v1/users/badges/:id/trackTrack badge progress by tracker ID
POST/v1/users/badges/:id/:badge_id/progressAdd progress for badge tracker
POST/v1/users/badges/:id/:badge_id/grantManually grant one-time badge
DELETE/v1/users/badges/:id/:badge_id/revokeRevoke granted badge
DELETE/v1/users/badges/:id/:badge_id/resetReset badge progress

Impact Points

MethodEndpointDescription
GET/v1/users/impact_points/:idGet points + history
POST/v1/users/impact_points/:id/addAdd points
POST/v1/users/impact_points/:id/removeRemove points
PUT/v1/users/impact_points/:id/setSet absolute points

Quests

MethodEndpointDescription
GET/v1/users/questsList quest definitions
GET/v1/users/quests/:idGet quest definition
POST/v1/users/quests/progress/:user_idStart quest
PATCH/v1/users/quests/progress/:user_idSubmit step response
GET/v1/users/quests/progress/:user_idGet active progress
GET/v1/users/quests/progress/:user_id/step/:step_indexGet specific step progress
DELETE/v1/users/quests/progress/:user_idReset active progress
GET/v1/users/quests/history/:user_idList completed quests
GET/v1/users/quests/history/:user_id/:quest_idGet completed quest payload

Events + Event Media

MethodEndpointDescription
GET/v1/events/thumbnail/:idGet event thumbnail image
GET/v1/events/thumbnail/:id/metadataGet thumbnail author + size metadata
POST/v1/events/thumbnail/:idUpload custom thumbnail
POST/v1/events/thumbnail/:id/generate?name={event_name}Generate location-based thumbnail
DELETE/v1/events/thumbnail/:idDelete event thumbnail
POST/v1/events/recommend_similar_eventsSimilar event recommendations
POST/v1/events/submit_imageSubmit 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

Behaviour Support

MethodEndpointDescription
GET/v1/users/:id/outdoor_nudgeWhether to nudge this user outside right now
GET/v1/users/:id/memoriesQuests and trails from this day in past years
GET/v1/users/:id/plan/statusWhether an if-then plan is live
GET/v1/users/:id/plan/outcomeMinutes outside since the plan was formed
POST/v1/users/:id/plan/menuBuild the cue/response menu for a plan
POST/v1/users/:id/plan/formLink one cue to one response, returned once
POST/v1/users/:id/plan/rehearseMark the single rehearsal
POST/v1/users/aliasRecord a public-id to numeric-id pair
POST/v1/users/:id/activitiesApply the consequences of an activity change

Content Analytics

Written by cloud's read timer and quest engine, and by mantle2 after a response. Read by the admin panels in crust and sky.

MethodEndpointDescription
GET/v1/content_analytics/individual/:idEvery category for one content id
GET/v1/content_analytics/user/:idAggregate across an owner's content
POST/v1/content_analytics/log_eventRecord a single event
POST/v1/content_analytics/log_timeRecord a timed event
DELETE/v1/content_analytics/deletePurge by content id or owner
GET/v1/admin/analyticsCloudflare traffic + signup funnel
POST/v1/admin/funnel/:fieldIncrement a signup funnel counter

WebSocket Routes

MethodEndpointDescription
POST/ws/notifyAdmin push payload to a channel
GET/ws/users/:id/ticketIssue 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 CaseModelRationale
Activity descriptions@cf/meta/llama-4-scout-17b-16e-instructRich descriptive generation
Activity tags@cf/meta/llama-3.1-8b-instruct-fp8Fast structured tagging
Article topic generation@cf/meta/llama-3.2-3b-instructLightweight topic selection
Semantic ranking (articles/events)@cf/baai/bge-reranker-baseStrong reranking quality
Article title + summary@cf/mistralai/mistral-small-3.1-24b-instructLong-form summarization quality
Article quiz generation@cf/meta/llama-4-scout-17b-16e-instructReliable structured question output
Prompt generation@cf/openai/gpt-oss-120bHigher-order prompt reasoning
Profile photo generation@cf/bytedance/stable-diffusion-xl-lightningFast image synthesis
Text embeddings for scoring@cf/baai/bge-m3Semantic similarity scoring
Image captioning for scoring@cf/llava-hf/llava-1.5-7b-hfVisual-to-text interpretation
Image classification@cf/microsoft/resnet-50Label confidence checks
Object detection@cf/facebook/detr-resnet-50Object-level validation
Audio transcription@cf/openai/whisper-large-v3-turboQuest 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)=>valinstanceofUint8Array ? {__type: 'Uint8Array',data: Array.from(val)} : val);JSON.parse(result,(_,val)=>(val?.__type==='Uint8Array' ? newUint8Array(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 });constresponse=awaitai.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

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

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:

typeBindings={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

The tables below cover the main surface. src/app.ts is the source of truth for the full route list.

Auth Model

  • All /v1/* endpoints require Authorization: Bearer {ADMIN_API_KEY}.
  • User and content ids accept either shape: the legacy numeric id, or the 32-hex public id mantle2 now issues. Cloud resolves a public id to the numeric one its KV is keyed on and caches the pair.
  • /ws/notify also requires admin bearer auth.
  • WebSocket user channels use session-validated one-time tickets (not admin keys).

Root

MethodEndpointDescription
GET/Health check (Woosh!)

Admin

MethodEndpointDescription
POST/v1/admin/migrate-legacy-keysMigrates legacy KV key formats

Activities

MethodEndpointDescription
GET/v1/activity/:idGenerate/retrieve activity metadata
GET/v1/synonyms?word={word}Retrieve synonyms for naming/aliasing

Articles

MethodEndpointDescription
GET/v1/articles/search?q={query}Search article sources
POST/v1/articles/recommend_similar_articlesSimilar article recommendations
POST/v1/articles/gradeAI rubric score for article text
POST/v1/articles/quiz/createGenerate and persist article quiz
GET/v1/articles/quiz?articleId={id}Fetch article quiz
POST/v1/articles/quiz/submitSubmit user quiz answers
GET/v1/articles/quiz/score?userId={id}&articleId={id}Fetch saved quiz score

Prompts

MethodEndpointDescription
POST/v1/prompts/gradeAI rubric score for prompt text

User Recommendations + Profiles

MethodEndpointDescription
POST/v1/users/recommend_activitiesActivity recommendations
POST/v1/users/recommend_articlesArticle recommendations by activities
POST/v1/users/recommend_eventsEvent recommendations by activities
GET/v1/users/profile_photo/:id?size={32|128|1024}Retrieve profile photo variant
PUT/v1/users/profile_photo/:idGenerate/replace profile photo
POST/v1/users/timerTimer Durable Object actions

Journeys

MethodEndpointDescription
GET/v1/users/journey/:type/:idGet journey streak + rank
POST/v1/users/journey/:type/:id/incrementIncrement streak with cooldown logic
DELETE/v1/users/journey/:type/:id/deleteReset streak
GET/v1/users/journey/:type/leaderboard?limit={n}Get top leaderboard
GET/v1/users/journey/:type/:id/rankGet user rank
GET/v1/users/journey/activity/:id/countCount completed activities
POST/v1/users/journey/activity/:id?activity={name}Add completed activity

Badges

MethodEndpointDescription
GET/v1/users/badgesList badge catalog
GET/v1/users/badges/:idList user's badge states
GET/v1/users/badges/:id/:badge_idGet single badge state
POST/v1/users/badges/:id/trackTrack badge progress by tracker ID
POST/v1/users/badges/:id/:badge_id/progressAdd progress for badge tracker
POST/v1/users/badges/:id/:badge_id/grantManually grant one-time badge
DELETE/v1/users/badges/:id/:badge_id/revokeRevoke granted badge
DELETE/v1/users/badges/:id/:badge_id/resetReset badge progress

Impact Points

MethodEndpointDescription
GET/v1/users/impact_points/:idGet points + history
POST/v1/users/impact_points/:id/addAdd points
POST/v1/users/impact_points/:id/removeRemove points
PUT/v1/users/impact_points/:id/setSet absolute points

Quests

MethodEndpointDescription
GET/v1/users/questsList quest definitions
GET/v1/users/quests/:idGet quest definition
POST/v1/users/quests/progress/:user_idStart quest
PATCH/v1/users/quests/progress/:user_idSubmit step response
GET/v1/users/quests/progress/:user_idGet active progress
GET/v1/users/quests/progress/:user_id/step/:step_indexGet specific step progress
DELETE/v1/users/quests/progress/:user_idReset active progress
GET/v1/users/quests/history/:user_idList completed quests
GET/v1/users/quests/history/:user_id/:quest_idGet completed quest payload

Events + Event Media

MethodEndpointDescription
GET/v1/events/thumbnail/:idGet event thumbnail image
GET/v1/events/thumbnail/:id/metadataGet thumbnail author + size metadata
POST/v1/events/thumbnail/:idUpload custom thumbnail
POST/v1/events/thumbnail/:id/generate?name={event_name}Generate location-based thumbnail
DELETE/v1/events/thumbnail/:idDelete event thumbnail
POST/v1/events/recommend_similar_eventsSimilar event recommendations
POST/v1/events/submit_imageSubmit 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

Behaviour Support

MethodEndpointDescription
GET/v1/users/:id/outdoor_nudgeWhether to nudge this user outside right now
GET/v1/users/:id/memoriesQuests and trails from this day in past years
GET/v1/users/:id/plan/statusWhether an if-then plan is live
GET/v1/users/:id/plan/outcomeMinutes outside since the plan was formed
POST/v1/users/:id/plan/menuBuild the cue/response menu for a plan
POST/v1/users/:id/plan/formLink one cue to one response, returned once
POST/v1/users/:id/plan/rehearseMark the single rehearsal
POST/v1/users/aliasRecord a public-id to numeric-id pair
POST/v1/users/:id/activitiesApply the consequences of an activity change

Content Analytics

Written by cloud's read timer and quest engine, and by mantle2 after a response. Read by the admin panels in crust and sky.

MethodEndpointDescription
GET/v1/content_analytics/individual/:idEvery category for one content id
GET/v1/content_analytics/user/:idAggregate across an owner's content
POST/v1/content_analytics/log_eventRecord a single event
POST/v1/content_analytics/log_timeRecord a timed event
DELETE/v1/content_analytics/deletePurge by content id or owner
GET/v1/admin/analyticsCloudflare traffic + signup funnel
POST/v1/admin/funnel/:fieldIncrement a signup funnel counter

WebSocket Routes

MethodEndpointDescription
POST/ws/notifyAdmin push payload to a channel
GET/ws/users/:id/ticketIssue 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 CaseModelRationale
Activity descriptions@cf/meta/llama-4-scout-17b-16e-instructRich descriptive generation
Activity tags@cf/meta/llama-3.1-8b-instruct-fp8Fast structured tagging
Article topic generation@cf/meta/llama-3.2-3b-instructLightweight topic selection
Semantic ranking (articles/events)@cf/baai/bge-reranker-baseStrong reranking quality
Article title + summary@cf/mistralai/mistral-small-3.1-24b-instructLong-form summarization quality
Article quiz generation@cf/meta/llama-4-scout-17b-16e-instructReliable structured question output
Prompt generation@cf/openai/gpt-oss-120bHigher-order prompt reasoning
Profile photo generation@cf/bytedance/stable-diffusion-xl-lightningFast image synthesis
Text embeddings for scoring@cf/baai/bge-m3Semantic similarity scoring
Image captioning for scoring@cf/llava-hf/llava-1.5-7b-hfVisual-to-text interpretation
Image classification@cf/microsoft/resnet-50Label confidence checks
Object detection@cf/facebook/detr-resnet-50Object-level validation
Audio transcription@cf/openai/whisper-large-v3-turboQuest 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)=>valinstanceofUint8Array ? {__type: 'Uint8Array',data: Array.from(val)} : val);JSON.parse(result,(_,val)=>(val?.__type==='Uint8Array' ? newUint8Array(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 });constresponse=awaitai.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

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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:

typeBindings={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

The tables below cover the main surface. src/app.ts is the source of truth for the full route list.

Auth Model

  • All /v1/* endpoints require Authorization: Bearer {ADMIN_API_KEY}.
  • User and content ids accept either shape: the legacy numeric id, or the 32-hex public id mantle2 now issues. Cloud resolves a public id to the numeric one its KV is keyed on and caches the pair.
  • /ws/notify also requires admin bearer auth.
  • WebSocket user channels use session-validated one-time tickets (not admin keys).

Root

MethodEndpointDescription
GET/Health check (Woosh!)

Admin

MethodEndpointDescription
POST/v1/admin/migrate-legacy-keysMigrates legacy KV key formats

Activities

MethodEndpointDescription
GET/v1/activity/:idGenerate/retrieve activity metadata
GET/v1/synonyms?word={word}Retrieve synonyms for naming/aliasing

Articles

MethodEndpointDescription
GET/v1/articles/search?q={query}Search article sources
POST/v1/articles/recommend_similar_articlesSimilar article recommendations
POST/v1/articles/gradeAI rubric score for article text
POST/v1/articles/quiz/createGenerate and persist article quiz
GET/v1/articles/quiz?articleId={id}Fetch article quiz
POST/v1/articles/quiz/submitSubmit user quiz answers
GET/v1/articles/quiz/score?userId={id}&articleId={id}Fetch saved quiz score

Prompts

MethodEndpointDescription
POST/v1/prompts/gradeAI rubric score for prompt text

User Recommendations + Profiles

MethodEndpointDescription
POST/v1/users/recommend_activitiesActivity recommendations
POST/v1/users/recommend_articlesArticle recommendations by activities
POST/v1/users/recommend_eventsEvent recommendations by activities
GET/v1/users/profile_photo/:id?size={32|128|1024}Retrieve profile photo variant
PUT/v1/users/profile_photo/:idGenerate/replace profile photo
POST/v1/users/timerTimer Durable Object actions

Journeys

MethodEndpointDescription
GET/v1/users/journey/:type/:idGet journey streak + rank
POST/v1/users/journey/:type/:id/incrementIncrement streak with cooldown logic
DELETE/v1/users/journey/:type/:id/deleteReset streak
GET/v1/users/journey/:type/leaderboard?limit={n}Get top leaderboard
GET/v1/users/journey/:type/:id/rankGet user rank
GET/v1/users/journey/activity/:id/countCount completed activities
POST/v1/users/journey/activity/:id?activity={name}Add completed activity

Badges

MethodEndpointDescription
GET/v1/users/badgesList badge catalog
GET/v1/users/badges/:idList user's badge states
GET/v1/users/badges/:id/:badge_idGet single badge state
POST/v1/users/badges/:id/trackTrack badge progress by tracker ID
POST/v1/users/badges/:id/:badge_id/progressAdd progress for badge tracker
POST/v1/users/badges/:id/:badge_id/grantManually grant one-time badge
DELETE/v1/users/badges/:id/:badge_id/revokeRevoke granted badge
DELETE/v1/users/badges/:id/:badge_id/resetReset badge progress

Impact Points

MethodEndpointDescription
GET/v1/users/impact_points/:idGet points + history
POST/v1/users/impact_points/:id/addAdd points
POST/v1/users/impact_points/:id/removeRemove points
PUT/v1/users/impact_points/:id/setSet absolute points

Quests

MethodEndpointDescription
GET/v1/users/questsList quest definitions
GET/v1/users/quests/:idGet quest definition
POST/v1/users/quests/progress/:user_idStart quest
PATCH/v1/users/quests/progress/:user_idSubmit step response
GET/v1/users/quests/progress/:user_idGet active progress
GET/v1/users/quests/progress/:user_id/step/:step_indexGet specific step progress
DELETE/v1/users/quests/progress/:user_idReset active progress
GET/v1/users/quests/history/:user_idList completed quests
GET/v1/users/quests/history/:user_id/:quest_idGet completed quest payload

Events + Event Media

MethodEndpointDescription
GET/v1/events/thumbnail/:idGet event thumbnail image
GET/v1/events/thumbnail/:id/metadataGet thumbnail author + size metadata
POST/v1/events/thumbnail/:idUpload custom thumbnail
POST/v1/events/thumbnail/:id/generate?name={event_name}Generate location-based thumbnail
DELETE/v1/events/thumbnail/:idDelete event thumbnail
POST/v1/events/recommend_similar_eventsSimilar event recommendations
POST/v1/events/submit_imageSubmit 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

Behaviour Support

MethodEndpointDescription
GET/v1/users/:id/outdoor_nudgeWhether to nudge this user outside right now
GET/v1/users/:id/memoriesQuests and trails from this day in past years
GET/v1/users/:id/plan/statusWhether an if-then plan is live
GET/v1/users/:id/plan/outcomeMinutes outside since the plan was formed
POST/v1/users/:id/plan/menuBuild the cue/response menu for a plan
POST/v1/users/:id/plan/formLink one cue to one response, returned once
POST/v1/users/:id/plan/rehearseMark the single rehearsal
POST/v1/users/aliasRecord a public-id to numeric-id pair
POST/v1/users/:id/activitiesApply the consequences of an activity change

Content Analytics

Written by cloud's read timer and quest engine, and by mantle2 after a response. Read by the admin panels in crust and sky.

MethodEndpointDescription
GET/v1/content_analytics/individual/:idEvery category for one content id
GET/v1/content_analytics/user/:idAggregate across an owner's content
POST/v1/content_analytics/log_eventRecord a single event
POST/v1/content_analytics/log_timeRecord a timed event
DELETE/v1/content_analytics/deletePurge by content id or owner
GET/v1/admin/analyticsCloudflare traffic + signup funnel
POST/v1/admin/funnel/:fieldIncrement a signup funnel counter

WebSocket Routes

MethodEndpointDescription
POST/ws/notifyAdmin push payload to a channel
GET/ws/users/:id/ticketIssue 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 CaseModelRationale
Activity descriptions@cf/meta/llama-4-scout-17b-16e-instructRich descriptive generation
Activity tags@cf/meta/llama-3.1-8b-instruct-fp8Fast structured tagging
Article topic generation@cf/meta/llama-3.2-3b-instructLightweight topic selection
Semantic ranking (articles/events)@cf/baai/bge-reranker-baseStrong reranking quality
Article title + summary@cf/mistralai/mistral-small-3.1-24b-instructLong-form summarization quality
Article quiz generation@cf/meta/llama-4-scout-17b-16e-instructReliable structured question output
Prompt generation@cf/openai/gpt-oss-120bHigher-order prompt reasoning
Profile photo generation@cf/bytedance/stable-diffusion-xl-lightningFast image synthesis
Text embeddings for scoring@cf/baai/bge-m3Semantic similarity scoring
Image captioning for scoring@cf/llava-hf/llava-1.5-7b-hfVisual-to-text interpretation
Image classification@cf/microsoft/resnet-50Label confidence checks
Object detection@cf/facebook/detr-resnet-50Object-level validation
Audio transcription@cf/openai/whisper-large-v3-turboQuest 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)=>valinstanceofUint8Array ? {__type: 'Uint8Array',data: Array.from(val)} : val);JSON.parse(result,(_,val)=>(val?.__type==='Uint8Array' ? newUint8Array(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 });constresponse=awaitai.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

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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:

typeBindings={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

The tables below cover the main surface. src/app.ts is the source of truth for the full route list.

Auth Model

  • All /v1/* endpoints require Authorization: Bearer {ADMIN_API_KEY}.
  • User and content ids accept either shape: the legacy numeric id, or the 32-hex public id mantle2 now issues. Cloud resolves a public id to the numeric one its KV is keyed on and caches the pair.
  • /ws/notify also requires admin bearer auth.
  • WebSocket user channels use session-validated one-time tickets (not admin keys).

Root

MethodEndpointDescription
GET/Health check (Woosh!)

Admin

MethodEndpointDescription
POST/v1/admin/migrate-legacy-keysMigrates legacy KV key formats

Activities

MethodEndpointDescription
GET/v1/activity/:idGenerate/retrieve activity metadata
GET/v1/synonyms?word={word}Retrieve synonyms for naming/aliasing

Articles

MethodEndpointDescription
GET/v1/articles/search?q={query}Search article sources
POST/v1/articles/recommend_similar_articlesSimilar article recommendations
POST/v1/articles/gradeAI rubric score for article text
POST/v1/articles/quiz/createGenerate and persist article quiz
GET/v1/articles/quiz?articleId={id}Fetch article quiz
POST/v1/articles/quiz/submitSubmit user quiz answers
GET/v1/articles/quiz/score?userId={id}&articleId={id}Fetch saved quiz score

Prompts

MethodEndpointDescription
POST/v1/prompts/gradeAI rubric score for prompt text

User Recommendations + Profiles

MethodEndpointDescription
POST/v1/users/recommend_activitiesActivity recommendations
POST/v1/users/recommend_articlesArticle recommendations by activities
POST/v1/users/recommend_eventsEvent recommendations by activities
GET/v1/users/profile_photo/:id?size={32|128|1024}Retrieve profile photo variant
PUT/v1/users/profile_photo/:idGenerate/replace profile photo
POST/v1/users/timerTimer Durable Object actions

Journeys

MethodEndpointDescription
GET/v1/users/journey/:type/:idGet journey streak + rank
POST/v1/users/journey/:type/:id/incrementIncrement streak with cooldown logic
DELETE/v1/users/journey/:type/:id/deleteReset streak
GET/v1/users/journey/:type/leaderboard?limit={n}Get top leaderboard
GET/v1/users/journey/:type/:id/rankGet user rank
GET/v1/users/journey/activity/:id/countCount completed activities
POST/v1/users/journey/activity/:id?activity={name}Add completed activity

Badges

MethodEndpointDescription
GET/v1/users/badgesList badge catalog
GET/v1/users/badges/:idList user's badge states
GET/v1/users/badges/:id/:badge_idGet single badge state
POST/v1/users/badges/:id/trackTrack badge progress by tracker ID
POST/v1/users/badges/:id/:badge_id/progressAdd progress for badge tracker
POST/v1/users/badges/:id/:badge_id/grantManually grant one-time badge
DELETE/v1/users/badges/:id/:badge_id/revokeRevoke granted badge
DELETE/v1/users/badges/:id/:badge_id/resetReset badge progress

Impact Points

MethodEndpointDescription
GET/v1/users/impact_points/:idGet points + history
POST/v1/users/impact_points/:id/addAdd points
POST/v1/users/impact_points/:id/removeRemove points
PUT/v1/users/impact_points/:id/setSet absolute points

Quests

MethodEndpointDescription
GET/v1/users/questsList quest definitions
GET/v1/users/quests/:idGet quest definition
POST/v1/users/quests/progress/:user_idStart quest
PATCH/v1/users/quests/progress/:user_idSubmit step response
GET/v1/users/quests/progress/:user_idGet active progress
GET/v1/users/quests/progress/:user_id/step/:step_indexGet specific step progress
DELETE/v1/users/quests/progress/:user_idReset active progress
GET/v1/users/quests/history/:user_idList completed quests
GET/v1/users/quests/history/:user_id/:quest_idGet completed quest payload

Events + Event Media

MethodEndpointDescription
GET/v1/events/thumbnail/:idGet event thumbnail image
GET/v1/events/thumbnail/:id/metadataGet thumbnail author + size metadata
POST/v1/events/thumbnail/:idUpload custom thumbnail
POST/v1/events/thumbnail/:id/generate?name={event_name}Generate location-based thumbnail
DELETE/v1/events/thumbnail/:idDelete event thumbnail
POST/v1/events/recommend_similar_eventsSimilar event recommendations
POST/v1/events/submit_imageSubmit 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

Behaviour Support

MethodEndpointDescription
GET/v1/users/:id/outdoor_nudgeWhether to nudge this user outside right now
GET/v1/users/:id/memoriesQuests and trails from this day in past years
GET/v1/users/:id/plan/statusWhether an if-then plan is live
GET/v1/users/:id/plan/outcomeMinutes outside since the plan was formed
POST/v1/users/:id/plan/menuBuild the cue/response menu for a plan
POST/v1/users/:id/plan/formLink one cue to one response, returned once
POST/v1/users/:id/plan/rehearseMark the single rehearsal
POST/v1/users/aliasRecord a public-id to numeric-id pair
POST/v1/users/:id/activitiesApply the consequences of an activity change

Content Analytics

Written by cloud's read timer and quest engine, and by mantle2 after a response. Read by the admin panels in crust and sky.

MethodEndpointDescription
GET/v1/content_analytics/individual/:idEvery category for one content id
GET/v1/content_analytics/user/:idAggregate across an owner's content
POST/v1/content_analytics/log_eventRecord a single event
POST/v1/content_analytics/log_timeRecord a timed event
DELETE/v1/content_analytics/deletePurge by content id or owner
GET/v1/admin/analyticsCloudflare traffic + signup funnel
POST/v1/admin/funnel/:fieldIncrement a signup funnel counter

WebSocket Routes

MethodEndpointDescription
POST/ws/notifyAdmin push payload to a channel
GET/ws/users/:id/ticketIssue 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 CaseModelRationale
Activity descriptions@cf/meta/llama-4-scout-17b-16e-instructRich descriptive generation
Activity tags@cf/meta/llama-3.1-8b-instruct-fp8Fast structured tagging
Article topic generation@cf/meta/llama-3.2-3b-instructLightweight topic selection
Semantic ranking (articles/events)@cf/baai/bge-reranker-baseStrong reranking quality
Article title + summary@cf/mistralai/mistral-small-3.1-24b-instructLong-form summarization quality
Article quiz generation@cf/meta/llama-4-scout-17b-16e-instructReliable structured question output
Prompt generation@cf/openai/gpt-oss-120bHigher-order prompt reasoning
Profile photo generation@cf/bytedance/stable-diffusion-xl-lightningFast image synthesis
Text embeddings for scoring@cf/baai/bge-m3Semantic similarity scoring
Image captioning for scoring@cf/llava-hf/llava-1.5-7b-hfVisual-to-text interpretation
Image classification@cf/microsoft/resnet-50Label confidence checks
Object detection@cf/facebook/detr-resnet-50Object-level validation
Audio transcription@cf/openai/whisper-large-v3-turboQuest 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)=>valinstanceofUint8Array ? {__type: 'Uint8Array',data: Array.from(val)} : val);JSON.parse(result,(_,val)=>(val?.__type==='Uint8Array' ? newUint8Array(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 });constresponse=awaitai.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

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

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:

typeBindings={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

The tables below cover the main surface. src/app.ts is the source of truth for the full route list.

Auth Model

  • All /v1/* endpoints require Authorization: Bearer {ADMIN_API_KEY}.
  • User and content ids accept either shape: the legacy numeric id, or the 32-hex public id mantle2 now issues. Cloud resolves a public id to the numeric one its KV is keyed on and caches the pair.
  • /ws/notify also requires admin bearer auth.
  • WebSocket user channels use session-validated one-time tickets (not admin keys).

Root

MethodEndpointDescription
GET/Health check (Woosh!)

Admin

MethodEndpointDescription
POST/v1/admin/migrate-legacy-keysMigrates legacy KV key formats

Activities

MethodEndpointDescription
GET/v1/activity/:idGenerate/retrieve activity metadata
GET/v1/synonyms?word={word}Retrieve synonyms for naming/aliasing

Articles

MethodEndpointDescription
GET/v1/articles/search?q={query}Search article sources
POST/v1/articles/recommend_similar_articlesSimilar article recommendations
POST/v1/articles/gradeAI rubric score for article text
POST/v1/articles/quiz/createGenerate and persist article quiz
GET/v1/articles/quiz?articleId={id}Fetch article quiz
POST/v1/articles/quiz/submitSubmit user quiz answers
GET/v1/articles/quiz/score?userId={id}&articleId={id}Fetch saved quiz score

Prompts

MethodEndpointDescription
POST/v1/prompts/gradeAI rubric score for prompt text

User Recommendations + Profiles

MethodEndpointDescription
POST/v1/users/recommend_activitiesActivity recommendations
POST/v1/users/recommend_articlesArticle recommendations by activities
POST/v1/users/recommend_eventsEvent recommendations by activities
GET/v1/users/profile_photo/:id?size={32|128|1024}Retrieve profile photo variant
PUT/v1/users/profile_photo/:idGenerate/replace profile photo
POST/v1/users/timerTimer Durable Object actions

Journeys

MethodEndpointDescription
GET/v1/users/journey/:type/:idGet journey streak + rank
POST/v1/users/journey/:type/:id/incrementIncrement streak with cooldown logic
DELETE/v1/users/journey/:type/:id/deleteReset streak
GET/v1/users/journey/:type/leaderboard?limit={n}Get top leaderboard
GET/v1/users/journey/:type/:id/rankGet user rank
GET/v1/users/journey/activity/:id/countCount completed activities
POST/v1/users/journey/activity/:id?activity={name}Add completed activity

Badges

MethodEndpointDescription
GET/v1/users/badgesList badge catalog
GET/v1/users/badges/:idList user's badge states
GET/v1/users/badges/:id/:badge_idGet single badge state
POST/v1/users/badges/:id/trackTrack badge progress by tracker ID
POST/v1/users/badges/:id/:badge_id/progressAdd progress for badge tracker
POST/v1/users/badges/:id/:badge_id/grantManually grant one-time badge
DELETE/v1/users/badges/:id/:badge_id/revokeRevoke granted badge
DELETE/v1/users/badges/:id/:badge_id/resetReset badge progress

Impact Points

MethodEndpointDescription
GET/v1/users/impact_points/:idGet points + history
POST/v1/users/impact_points/:id/addAdd points
POST/v1/users/impact_points/:id/removeRemove points
PUT/v1/users/impact_points/:id/setSet absolute points

Quests

MethodEndpointDescription
GET/v1/users/questsList quest definitions
GET/v1/users/quests/:idGet quest definition
POST/v1/users/quests/progress/:user_idStart quest
PATCH/v1/users/quests/progress/:user_idSubmit step response
GET/v1/users/quests/progress/:user_idGet active progress
GET/v1/users/quests/progress/:user_id/step/:step_indexGet specific step progress
DELETE/v1/users/quests/progress/:user_idReset active progress
GET/v1/users/quests/history/:user_idList completed quests
GET/v1/users/quests/history/:user_id/:quest_idGet completed quest payload

Events + Event Media

MethodEndpointDescription
GET/v1/events/thumbnail/:idGet event thumbnail image
GET/v1/events/thumbnail/:id/metadataGet thumbnail author + size metadata
POST/v1/events/thumbnail/:idUpload custom thumbnail
POST/v1/events/thumbnail/:id/generate?name={event_name}Generate location-based thumbnail
DELETE/v1/events/thumbnail/:idDelete event thumbnail
POST/v1/events/recommend_similar_eventsSimilar event recommendations
POST/v1/events/submit_imageSubmit 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

Behaviour Support

MethodEndpointDescription
GET/v1/users/:id/outdoor_nudgeWhether to nudge this user outside right now
GET/v1/users/:id/memoriesQuests and trails from this day in past years
GET/v1/users/:id/plan/statusWhether an if-then plan is live
GET/v1/users/:id/plan/outcomeMinutes outside since the plan was formed
POST/v1/users/:id/plan/menuBuild the cue/response menu for a plan
POST/v1/users/:id/plan/formLink one cue to one response, returned once
POST/v1/users/:id/plan/rehearseMark the single rehearsal
POST/v1/users/aliasRecord a public-id to numeric-id pair
POST/v1/users/:id/activitiesApply the consequences of an activity change

Content Analytics

Written by cloud's read timer and quest engine, and by mantle2 after a response. Read by the admin panels in crust and sky.

MethodEndpointDescription
GET/v1/content_analytics/individual/:idEvery category for one content id
GET/v1/content_analytics/user/:idAggregate across an owner's content
POST/v1/content_analytics/log_eventRecord a single event
POST/v1/content_analytics/log_timeRecord a timed event
DELETE/v1/content_analytics/deletePurge by content id or owner
GET/v1/admin/analyticsCloudflare traffic + signup funnel
POST/v1/admin/funnel/:fieldIncrement a signup funnel counter

WebSocket Routes

MethodEndpointDescription
POST/ws/notifyAdmin push payload to a channel
GET/ws/users/:id/ticketIssue 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 CaseModelRationale
Activity descriptions@cf/meta/llama-4-scout-17b-16e-instructRich descriptive generation
Activity tags@cf/meta/llama-3.1-8b-instruct-fp8Fast structured tagging
Article topic generation@cf/meta/llama-3.2-3b-instructLightweight topic selection
Semantic ranking (articles/events)@cf/baai/bge-reranker-baseStrong reranking quality
Article title + summary@cf/mistralai/mistral-small-3.1-24b-instructLong-form summarization quality
Article quiz generation@cf/meta/llama-4-scout-17b-16e-instructReliable structured question output
Prompt generation@cf/openai/gpt-oss-120bHigher-order prompt reasoning
Profile photo generation@cf/bytedance/stable-diffusion-xl-lightningFast image synthesis
Text embeddings for scoring@cf/baai/bge-m3Semantic similarity scoring
Image captioning for scoring@cf/llava-hf/llava-1.5-7b-hfVisual-to-text interpretation
Image classification@cf/microsoft/resnet-50Label confidence checks
Object detection@cf/facebook/detr-resnet-50Object-level validation
Audio transcription@cf/openai/whisper-large-v3-turboQuest 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)=>valinstanceofUint8Array ? {__type: 'Uint8Array',data: Array.from(val)} : val);JSON.parse(result,(_,val)=>(val?.__type==='Uint8Array' ? newUint8Array(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 });constresponse=awaitai.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

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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:

typeBindings={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

The tables below cover the main surface. src/app.ts is the source of truth for the full route list.

Auth Model

  • All /v1/* endpoints require Authorization: Bearer {ADMIN_API_KEY}.
  • User and content ids accept either shape: the legacy numeric id, or the 32-hex public id mantle2 now issues. Cloud resolves a public id to the numeric one its KV is keyed on and caches the pair.
  • /ws/notify also requires admin bearer auth.
  • WebSocket user channels use session-validated one-time tickets (not admin keys).

Root

MethodEndpointDescription
GET/Health check (Woosh!)

Admin

MethodEndpointDescription
POST/v1/admin/migrate-legacy-keysMigrates legacy KV key formats

Activities

MethodEndpointDescription
GET/v1/activity/:idGenerate/retrieve activity metadata
GET/v1/synonyms?word={word}Retrieve synonyms for naming/aliasing

Articles

MethodEndpointDescription
GET/v1/articles/search?q={query}Search article sources
POST/v1/articles/recommend_similar_articlesSimilar article recommendations
POST/v1/articles/gradeAI rubric score for article text
POST/v1/articles/quiz/createGenerate and persist article quiz
GET/v1/articles/quiz?articleId={id}Fetch article quiz
POST/v1/articles/quiz/submitSubmit user quiz answers
GET/v1/articles/quiz/score?userId={id}&articleId={id}Fetch saved quiz score

Prompts

MethodEndpointDescription
POST/v1/prompts/gradeAI rubric score for prompt text

User Recommendations + Profiles

MethodEndpointDescription
POST/v1/users/recommend_activitiesActivity recommendations
POST/v1/users/recommend_articlesArticle recommendations by activities
POST/v1/users/recommend_eventsEvent recommendations by activities
GET/v1/users/profile_photo/:id?size={32|128|1024}Retrieve profile photo variant
PUT/v1/users/profile_photo/:idGenerate/replace profile photo
POST/v1/users/timerTimer Durable Object actions

Journeys

MethodEndpointDescription
GET/v1/users/journey/:type/:idGet journey streak + rank
POST/v1/users/journey/:type/:id/incrementIncrement streak with cooldown logic
DELETE/v1/users/journey/:type/:id/deleteReset streak
GET/v1/users/journey/:type/leaderboard?limit={n}Get top leaderboard
GET/v1/users/journey/:type/:id/rankGet user rank
GET/v1/users/journey/activity/:id/countCount completed activities
POST/v1/users/journey/activity/:id?activity={name}Add completed activity

Badges

MethodEndpointDescription
GET/v1/users/badgesList badge catalog
GET/v1/users/badges/:idList user's badge states
GET/v1/users/badges/:id/:badge_idGet single badge state
POST/v1/users/badges/:id/trackTrack badge progress by tracker ID
POST/v1/users/badges/:id/:badge_id/progressAdd progress for badge tracker
POST/v1/users/badges/:id/:badge_id/grantManually grant one-time badge
DELETE/v1/users/badges/:id/:badge_id/revokeRevoke granted badge
DELETE/v1/users/badges/:id/:badge_id/resetReset badge progress

Impact Points

MethodEndpointDescription
GET/v1/users/impact_points/:idGet points + history
POST/v1/users/impact_points/:id/addAdd points
POST/v1/users/impact_points/:id/removeRemove points
PUT/v1/users/impact_points/:id/setSet absolute points

Quests

MethodEndpointDescription
GET/v1/users/questsList quest definitions
GET/v1/users/quests/:idGet quest definition
POST/v1/users/quests/progress/:user_idStart quest
PATCH/v1/users/quests/progress/:user_idSubmit step response
GET/v1/users/quests/progress/:user_idGet active progress
GET/v1/users/quests/progress/:user_id/step/:step_indexGet specific step progress
DELETE/v1/users/quests/progress/:user_idReset active progress
GET/v1/users/quests/history/:user_idList completed quests
GET/v1/users/quests/history/:user_id/:quest_idGet completed quest payload

Events + Event Media

MethodEndpointDescription
GET/v1/events/thumbnail/:idGet event thumbnail image
GET/v1/events/thumbnail/:id/metadataGet thumbnail author + size metadata
POST/v1/events/thumbnail/:idUpload custom thumbnail
POST/v1/events/thumbnail/:id/generate?name={event_name}Generate location-based thumbnail
DELETE/v1/events/thumbnail/:idDelete event thumbnail
POST/v1/events/recommend_similar_eventsSimilar event recommendations
POST/v1/events/submit_imageSubmit 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

Behaviour Support

MethodEndpointDescription
GET/v1/users/:id/outdoor_nudgeWhether to nudge this user outside right now
GET/v1/users/:id/memoriesQuests and trails from this day in past years
GET/v1/users/:id/plan/statusWhether an if-then plan is live
GET/v1/users/:id/plan/outcomeMinutes outside since the plan was formed
POST/v1/users/:id/plan/menuBuild the cue/response menu for a plan
POST/v1/users/:id/plan/formLink one cue to one response, returned once
POST/v1/users/:id/plan/rehearseMark the single rehearsal
POST/v1/users/aliasRecord a public-id to numeric-id pair
POST/v1/users/:id/activitiesApply the consequences of an activity change

Content Analytics

Written by cloud's read timer and quest engine, and by mantle2 after a response. Read by the admin panels in crust and sky.

MethodEndpointDescription
GET/v1/content_analytics/individual/:idEvery category for one content id
GET/v1/content_analytics/user/:idAggregate across an owner's content
POST/v1/content_analytics/log_eventRecord a single event
POST/v1/content_analytics/log_timeRecord a timed event
DELETE/v1/content_analytics/deletePurge by content id or owner
GET/v1/admin/analyticsCloudflare traffic + signup funnel
POST/v1/admin/funnel/:fieldIncrement a signup funnel counter

WebSocket Routes

MethodEndpointDescription
POST/ws/notifyAdmin push payload to a channel
GET/ws/users/:id/ticketIssue 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 CaseModelRationale
Activity descriptions@cf/meta/llama-4-scout-17b-16e-instructRich descriptive generation
Activity tags@cf/meta/llama-3.1-8b-instruct-fp8Fast structured tagging
Article topic generation@cf/meta/llama-3.2-3b-instructLightweight topic selection
Semantic ranking (articles/events)@cf/baai/bge-reranker-baseStrong reranking quality
Article title + summary@cf/mistralai/mistral-small-3.1-24b-instructLong-form summarization quality
Article quiz generation@cf/meta/llama-4-scout-17b-16e-instructReliable structured question output
Prompt generation@cf/openai/gpt-oss-120bHigher-order prompt reasoning
Profile photo generation@cf/bytedance/stable-diffusion-xl-lightningFast image synthesis
Text embeddings for scoring@cf/baai/bge-m3Semantic similarity scoring
Image captioning for scoring@cf/llava-hf/llava-1.5-7b-hfVisual-to-text interpretation
Image classification@cf/microsoft/resnet-50Label confidence checks
Object detection@cf/facebook/detr-resnet-50Object-level validation
Audio transcription@cf/openai/whisper-large-v3-turboQuest 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)=>valinstanceofUint8Array ? {__type: 'Uint8Array',data: Array.from(val)} : val);JSON.parse(result,(_,val)=>(val?.__type==='Uint8Array' ? newUint8Array(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 });constresponse=awaitai.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

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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:

typeBindings={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

The tables below cover the main surface. src/app.ts is the source of truth for the full route list.

Auth Model

  • All /v1/* endpoints require Authorization: Bearer {ADMIN_API_KEY}.
  • User and content ids accept either shape: the legacy numeric id, or the 32-hex public id mantle2 now issues. Cloud resolves a public id to the numeric one its KV is keyed on and caches the pair.
  • /ws/notify also requires admin bearer auth.
  • WebSocket user channels use session-validated one-time tickets (not admin keys).

Root

MethodEndpointDescription
GET/Health check (Woosh!)

Admin

MethodEndpointDescription
POST/v1/admin/migrate-legacy-keysMigrates legacy KV key formats

Activities

MethodEndpointDescription
GET/v1/activity/:idGenerate/retrieve activity metadata
GET/v1/synonyms?word={word}Retrieve synonyms for naming/aliasing

Articles

MethodEndpointDescription
GET/v1/articles/search?q={query}Search article sources
POST/v1/articles/recommend_similar_articlesSimilar article recommendations
POST/v1/articles/gradeAI rubric score for article text
POST/v1/articles/quiz/createGenerate and persist article quiz
GET/v1/articles/quiz?articleId={id}Fetch article quiz
POST/v1/articles/quiz/submitSubmit user quiz answers
GET/v1/articles/quiz/score?userId={id}&articleId={id}Fetch saved quiz score

Prompts

MethodEndpointDescription
POST/v1/prompts/gradeAI rubric score for prompt text

User Recommendations + Profiles

MethodEndpointDescription
POST/v1/users/recommend_activitiesActivity recommendations
POST/v1/users/recommend_articlesArticle recommendations by activities
POST/v1/users/recommend_eventsEvent recommendations by activities
GET/v1/users/profile_photo/:id?size={32|128|1024}Retrieve profile photo variant
PUT/v1/users/profile_photo/:idGenerate/replace profile photo
POST/v1/users/timerTimer Durable Object actions

Journeys

MethodEndpointDescription
GET/v1/users/journey/:type/:idGet journey streak + rank
POST/v1/users/journey/:type/:id/incrementIncrement streak with cooldown logic
DELETE/v1/users/journey/:type/:id/deleteReset streak
GET/v1/users/journey/:type/leaderboard?limit={n}Get top leaderboard
GET/v1/users/journey/:type/:id/rankGet user rank
GET/v1/users/journey/activity/:id/countCount completed activities
POST/v1/users/journey/activity/:id?activity={name}Add completed activity

Badges

MethodEndpointDescription
GET/v1/users/badgesList badge catalog
GET/v1/users/badges/:idList user's badge states
GET/v1/users/badges/:id/:badge_idGet single badge state
POST/v1/users/badges/:id/trackTrack badge progress by tracker ID
POST/v1/users/badges/:id/:badge_id/progressAdd progress for badge tracker
POST/v1/users/badges/:id/:badge_id/grantManually grant one-time badge
DELETE/v1/users/badges/:id/:badge_id/revokeRevoke granted badge
DELETE/v1/users/badges/:id/:badge_id/resetReset badge progress

Impact Points

MethodEndpointDescription
GET/v1/users/impact_points/:idGet points + history
POST/v1/users/impact_points/:id/addAdd points
POST/v1/users/impact_points/:id/removeRemove points
PUT/v1/users/impact_points/:id/setSet absolute points

Quests

MethodEndpointDescription
GET/v1/users/questsList quest definitions
GET/v1/users/quests/:idGet quest definition
POST/v1/users/quests/progress/:user_idStart quest
PATCH/v1/users/quests/progress/:user_idSubmit step response
GET/v1/users/quests/progress/:user_idGet active progress
GET/v1/users/quests/progress/:user_id/step/:step_indexGet specific step progress
DELETE/v1/users/quests/progress/:user_idReset active progress
GET/v1/users/quests/history/:user_idList completed quests
GET/v1/users/quests/history/:user_id/:quest_idGet completed quest payload

Events + Event Media

MethodEndpointDescription
GET/v1/events/thumbnail/:idGet event thumbnail image
GET/v1/events/thumbnail/:id/metadataGet thumbnail author + size metadata
POST/v1/events/thumbnail/:idUpload custom thumbnail
POST/v1/events/thumbnail/:id/generate?name={event_name}Generate location-based thumbnail
DELETE/v1/events/thumbnail/:idDelete event thumbnail
POST/v1/events/recommend_similar_eventsSimilar event recommendations
POST/v1/events/submit_imageSubmit 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

Behaviour Support

MethodEndpointDescription
GET/v1/users/:id/outdoor_nudgeWhether to nudge this user outside right now
GET/v1/users/:id/memoriesQuests and trails from this day in past years
GET/v1/users/:id/plan/statusWhether an if-then plan is live
GET/v1/users/:id/plan/outcomeMinutes outside since the plan was formed
POST/v1/users/:id/plan/menuBuild the cue/response menu for a plan
POST/v1/users/:id/plan/formLink one cue to one response, returned once
POST/v1/users/:id/plan/rehearseMark the single rehearsal
POST/v1/users/aliasRecord a public-id to numeric-id pair
POST/v1/users/:id/activitiesApply the consequences of an activity change

Content Analytics

Written by cloud's read timer and quest engine, and by mantle2 after a response. Read by the admin panels in crust and sky.

MethodEndpointDescription
GET/v1/content_analytics/individual/:idEvery category for one content id
GET/v1/content_analytics/user/:idAggregate across an owner's content
POST/v1/content_analytics/log_eventRecord a single event
POST/v1/content_analytics/log_timeRecord a timed event
DELETE/v1/content_analytics/deletePurge by content id or owner
GET/v1/admin/analyticsCloudflare traffic + signup funnel
POST/v1/admin/funnel/:fieldIncrement a signup funnel counter

WebSocket Routes

MethodEndpointDescription
POST/ws/notifyAdmin push payload to a channel
GET/ws/users/:id/ticketIssue 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 CaseModelRationale
Activity descriptions@cf/meta/llama-4-scout-17b-16e-instructRich descriptive generation
Activity tags@cf/meta/llama-3.1-8b-instruct-fp8Fast structured tagging
Article topic generation@cf/meta/llama-3.2-3b-instructLightweight topic selection
Semantic ranking (articles/events)@cf/baai/bge-reranker-baseStrong reranking quality
Article title + summary@cf/mistralai/mistral-small-3.1-24b-instructLong-form summarization quality
Article quiz generation@cf/meta/llama-4-scout-17b-16e-instructReliable structured question output
Prompt generation@cf/openai/gpt-oss-120bHigher-order prompt reasoning
Profile photo generation@cf/bytedance/stable-diffusion-xl-lightningFast image synthesis
Text embeddings for scoring@cf/baai/bge-m3Semantic similarity scoring
Image captioning for scoring@cf/llava-hf/llava-1.5-7b-hfVisual-to-text interpretation
Image classification@cf/microsoft/resnet-50Label confidence checks
Object detection@cf/facebook/detr-resnet-50Object-level validation
Audio transcription@cf/openai/whisper-large-v3-turboQuest 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)=>valinstanceofUint8Array ? {__type: 'Uint8Array',data: Array.from(val)} : val);JSON.parse(result,(_,val)=>(val?.__type==='Uint8Array' ? newUint8Array(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 });constresponse=awaitai.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

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

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:

typeBindings={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

The tables below cover the main surface. src/app.ts is the source of truth for the full route list.

Auth Model

  • All /v1/* endpoints require Authorization: Bearer {ADMIN_API_KEY}.
  • User and content ids accept either shape: the legacy numeric id, or the 32-hex public id mantle2 now issues. Cloud resolves a public id to the numeric one its KV is keyed on and caches the pair.
  • /ws/notify also requires admin bearer auth.
  • WebSocket user channels use session-validated one-time tickets (not admin keys).

Root

MethodEndpointDescription
GET/Health check (Woosh!)

Admin

MethodEndpointDescription
POST/v1/admin/migrate-legacy-keysMigrates legacy KV key formats

Activities

MethodEndpointDescription
GET/v1/activity/:idGenerate/retrieve activity metadata
GET/v1/synonyms?word={word}Retrieve synonyms for naming/aliasing

Articles

MethodEndpointDescription
GET/v1/articles/search?q={query}Search article sources
POST/v1/articles/recommend_similar_articlesSimilar article recommendations
POST/v1/articles/gradeAI rubric score for article text
POST/v1/articles/quiz/createGenerate and persist article quiz
GET/v1/articles/quiz?articleId={id}Fetch article quiz
POST/v1/articles/quiz/submitSubmit user quiz answers
GET/v1/articles/quiz/score?userId={id}&articleId={id}Fetch saved quiz score

Prompts

MethodEndpointDescription
POST/v1/prompts/gradeAI rubric score for prompt text

User Recommendations + Profiles

MethodEndpointDescription
POST/v1/users/recommend_activitiesActivity recommendations
POST/v1/users/recommend_articlesArticle recommendations by activities
POST/v1/users/recommend_eventsEvent recommendations by activities
GET/v1/users/profile_photo/:id?size={32|128|1024}Retrieve profile photo variant
PUT/v1/users/profile_photo/:idGenerate/replace profile photo
POST/v1/users/timerTimer Durable Object actions

Journeys

MethodEndpointDescription
GET/v1/users/journey/:type/:idGet journey streak + rank
POST/v1/users/journey/:type/:id/incrementIncrement streak with cooldown logic
DELETE/v1/users/journey/:type/:id/deleteReset streak
GET/v1/users/journey/:type/leaderboard?limit={n}Get top leaderboard
GET/v1/users/journey/:type/:id/rankGet user rank
GET/v1/users/journey/activity/:id/countCount completed activities
POST/v1/users/journey/activity/:id?activity={name}Add completed activity

Badges

MethodEndpointDescription
GET/v1/users/badgesList badge catalog
GET/v1/users/badges/:idList user's badge states
GET/v1/users/badges/:id/:badge_idGet single badge state
POST/v1/users/badges/:id/trackTrack badge progress by tracker ID
POST/v1/users/badges/:id/:badge_id/progressAdd progress for badge tracker
POST/v1/users/badges/:id/:badge_id/grantManually grant one-time badge
DELETE/v1/users/badges/:id/:badge_id/revokeRevoke granted badge
DELETE/v1/users/badges/:id/:badge_id/resetReset badge progress

Impact Points

MethodEndpointDescription
GET/v1/users/impact_points/:idGet points + history
POST/v1/users/impact_points/:id/addAdd points
POST/v1/users/impact_points/:id/removeRemove points
PUT/v1/users/impact_points/:id/setSet absolute points

Quests

MethodEndpointDescription
GET/v1/users/questsList quest definitions
GET/v1/users/quests/:idGet quest definition
POST/v1/users/quests/progress/:user_idStart quest
PATCH/v1/users/quests/progress/:user_idSubmit step response
GET/v1/users/quests/progress/:user_idGet active progress
GET/v1/users/quests/progress/:user_id/step/:step_indexGet specific step progress
DELETE/v1/users/quests/progress/:user_idReset active progress
GET/v1/users/quests/history/:user_idList completed quests
GET/v1/users/quests/history/:user_id/:quest_idGet completed quest payload

Events + Event Media

MethodEndpointDescription
GET/v1/events/thumbnail/:idGet event thumbnail image
GET/v1/events/thumbnail/:id/metadataGet thumbnail author + size metadata
POST/v1/events/thumbnail/:idUpload custom thumbnail
POST/v1/events/thumbnail/:id/generate?name={event_name}Generate location-based thumbnail
DELETE/v1/events/thumbnail/:idDelete event thumbnail
POST/v1/events/recommend_similar_eventsSimilar event recommendations
POST/v1/events/submit_imageSubmit 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

Behaviour Support

MethodEndpointDescription
GET/v1/users/:id/outdoor_nudgeWhether to nudge this user outside right now
GET/v1/users/:id/memoriesQuests and trails from this day in past years
GET/v1/users/:id/plan/statusWhether an if-then plan is live
GET/v1/users/:id/plan/outcomeMinutes outside since the plan was formed
POST/v1/users/:id/plan/menuBuild the cue/response menu for a plan
POST/v1/users/:id/plan/formLink one cue to one response, returned once
POST/v1/users/:id/plan/rehearseMark the single rehearsal
POST/v1/users/aliasRecord a public-id to numeric-id pair
POST/v1/users/:id/activitiesApply the consequences of an activity change

Content Analytics

Written by cloud's read timer and quest engine, and by mantle2 after a response. Read by the admin panels in crust and sky.

MethodEndpointDescription
GET/v1/content_analytics/individual/:idEvery category for one content id
GET/v1/content_analytics/user/:idAggregate across an owner's content
POST/v1/content_analytics/log_eventRecord a single event
POST/v1/content_analytics/log_timeRecord a timed event
DELETE/v1/content_analytics/deletePurge by content id or owner
GET/v1/admin/analyticsCloudflare traffic + signup funnel
POST/v1/admin/funnel/:fieldIncrement a signup funnel counter

WebSocket Routes

MethodEndpointDescription
POST/ws/notifyAdmin push payload to a channel
GET/ws/users/:id/ticketIssue 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 CaseModelRationale
Activity descriptions@cf/meta/llama-4-scout-17b-16e-instructRich descriptive generation
Activity tags@cf/meta/llama-3.1-8b-instruct-fp8Fast structured tagging
Article topic generation@cf/meta/llama-3.2-3b-instructLightweight topic selection
Semantic ranking (articles/events)@cf/baai/bge-reranker-baseStrong reranking quality
Article title + summary@cf/mistralai/mistral-small-3.1-24b-instructLong-form summarization quality
Article quiz generation@cf/meta/llama-4-scout-17b-16e-instructReliable structured question output
Prompt generation@cf/openai/gpt-oss-120bHigher-order prompt reasoning
Profile photo generation@cf/bytedance/stable-diffusion-xl-lightningFast image synthesis
Text embeddings for scoring@cf/baai/bge-m3Semantic similarity scoring
Image captioning for scoring@cf/llava-hf/llava-1.5-7b-hfVisual-to-text interpretation
Image classification@cf/microsoft/resnet-50Label confidence checks
Object detection@cf/facebook/detr-resnet-50Object-level validation
Audio transcription@cf/openai/whisper-large-v3-turboQuest 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)=>valinstanceofUint8Array ? {__type: 'Uint8Array',data: Array.from(val)} : val);JSON.parse(result,(_,val)=>(val?.__type==='Uint8Array' ? newUint8Array(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 });constresponse=awaitai.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

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages