Modern, full-stack frontend application for The Earth App.
It serves as the user-facing web interface and API proxy layer, deployed on Cloudflare Workers via NuxtHub.
- Framework: Nuxt 4 (Vue 3 + Server-Side Rendering)
- Runtime: Bun (development & package management)
- Styling: Tailwind CSS v4 + @nuxt/ui
- State Management: Pinia stores + composables
- Type Safety: TypeScript with strict type checking
- Validation: Zod schemas for runtime validation
- Date/Time: Luxon for timezone-aware operations
- Deployment: Cloudflare Workers via NuxtHub
- Icons: @nuxt/icon with 17+ icon sets via Iconify
- Security: Cloudflare Turnstile for bot protection
- Backend Integration:
@earth-app/oceanshared types
src/
├── app.vue # Root component with global SEO meta
├── error.vue # Global error handling page
├── assets/css/ # Global styles (Tailwind config)
├── components/ # Reusable Vue components
├── composables/ # Composable functions (domain logic + store orchestration)
├── layouts/ # Page layouts (default.vue)
├── pages/ # File-based routing
├── stores/ # Pinia stores (auth, users, content caches)
├── server/ # Nitro server routes (API proxy)
│ ├── api/ # API endpoints
│ └── utils.ts # Server utilities (auth guards)
└── shared/ # Shared types & utilitiesCrust leverages Nuxt's flexible rendering modes for optimal performance:
- SSR (Server-Side Rendering): Default for SEO-critical pages
- ISR (Incremental Static Regeneration): For content listing pages
- Homepage: regenerates every hour (
isr: 3600) - Activities: every 4 hours (
isr: 14400) - Events: every 10 minutes (
isr: 600) - Prompts: every 15 minutes (
isr: 900)
- Homepage: regenerates every hour (
- SWR (Stale-While-Revalidate): For individual content pages
- Activities cached for 4 hours
- Articles cached for 1 hour
- Events cached for 30 minutes
- Prompts cached for 30 minutes
- Client-Side Only: For authenticated pages (profiles, admin, auth flows)
// nuxt.config.ts
routeRules: {
'/': { isr: 3600 }, // ISR for homepage
'/activities/**': { swr: 14400 }, // SWR for activity pages
'/events/**': { swr: 1800 }, // SWR for event pages
'/profile/**': { ssr: false }, // Client-only for user pages
'/api/**': { cors: false }, // CORS handled via server middleware
'/api/activity/**': { cache: { maxAge: 3600 } }
}The server layer (src/server/api/) acts as a secure proxy to:
- External APIs: Wikipedia, YouTube, Iconify, Pixabay, Google Maps/Places
- Backend Services: The Earth App API (
/v2/*) and cloud recommendation services - Third-Party Services: Cloudflare Turnstile verification
Benefits:
- Hides API keys from client-side code
- Implements server-side authentication guards
- Provides caching and request shaping
- Centralizes CORS policy in server middleware
Example: Icon search endpoint (src/server/api/activity/iconSearch.get.ts)
export default defineEventHandler(async (event) => {
const { search } = getQuery(event);
// Server-side fetch to Iconify API
const response = await $fetch(`https://api.iconify.design/search?query=${search}`, {
headers: { 'User-Agent': 'The Earth App/Web' }
});
return response;
});- Token-based Auth: JWT session tokens synced via secure cookies and validated server-side
- Store-Backed Session State:
useAuthStore()(Pinia) manages user/session loading and auth state - Composable Hooks:
useAuth(): Current user state & avatar managementuseCurrentSessionToken(): Session token retrieval
- Server Guards (
src/server/utils.ts):ensureLoggedIn(): Validates user authenticationensureAdministrator(): Restricts admin-only routesensureValidActivity(): Validates activity existence
Example: Admin route protection
export default defineEventHandler(async (event) => {
await ensureAdministrator(event); // Throws 403 if not admin
// Admin logic here...
});Session Flow:
- Session token is persisted in a
session_tokencookie (Secure,SameSite=None) - Client bootstraps auth state through
/api/auth/session useAuthStore()fetches/v2/users/currentand keeps auth state reactive across pages- OAuth callback routes can issue/update session cookies when linking or signing in
All types are sourced from @earth-app/ocean (Protocol Buffers) and extended locally:
// src/shared/types/user.ts
import { com } from '@earth-app/ocean';
export type User = {
id: string;
username: string;
account: {
account_type: typeof com.earthapp.account.AccountType.prototype.name;
visibility: typeof com.earthapp.Visibility.prototype.name;
field_privacy: {
/* Privacy settings */
};
};
// ...
};Runtime validation with Zod (src/shared/utils/schemas.ts):
export const usernameSchema = z
.string()
.min(3, 'Must be at least 3 characters')
.max(30, 'Must be at most 30 characters')
.regex(/^[a-zA-Z0-9_.-]+$/, 'Invalid characters');
export const passwordSchema = z
.string()
.min(8, 'Must be at least 8 characters')
.max(100, 'Must be at most 100 characters');Centralized request handling in src/shared/utils/util.ts:
makeRequest<T>(): Base request wrapper with dedupe + cachemakeAPIRequest<T>(): Typed backend API requestsmakeClientAPIRequest<T>(): Client-side only requestsmakeServerRequest<T>(): Server-to-server communicationpaginatedAPIRequest<T>(): Automatic pagination handling
Features:
- Automatic error handling (404, 401, 429, 500)
- Binary data support (profile photos)
- Token injection
- Request deduplication queue
- In-memory LRU response cache (bounded)
Example:
export async function getActivity(id: string) {
return await makeAPIRequest<Activity>(
`activity-${id}`, // Cache key
`/v2/activities/${id}`, // API path
useCurrentSessionToken() // Auth token
);
}activity/: Activity cards, profiles, admin editorsadmin/: Activity editor, modal dialogsarticle/: Article cards, full-page readersevent/: Event cards, editors, location and submission flowsprompt/: Prompt cards, creation menus, responsesuser/: Profiles, login/signup forms, settings modals
InfoCard&InfoCardGroup: Content display gridsEarthCircle: Animated homepage logoTurnstileWidget: Cloudflare captcha integrationSiteTour: Guided onboarding (viauseSiteTour())
Configured in app.vue with useSeoMeta():
- Open Graph tags for social sharing
- Twitter Card metadata
- Dynamic titles via
useTitleSuffix() - Application metadata (theme colors, mobile-web-app-capable)
Sitemap Generation:
- Prerendered at build time (
/sitemap.xml) - Configured via
@nuxtjs/sitemapmodule
Robots.txt:
- Static file at
public/_robots.txt - Managed by
@nuxtjs/robotsmodule - Currently allows crawling by default
Configured via @nuxtjs/i18n:
- Current: English (
en-US) - Prepared for multi-language expansion
- Locale-aware routing
- Prettier: Auto-formatting on commit (via Husky + lint-staged)
- TypeScript: Strict mode enabled
- Bun: Fast package installation & script execution
{
"dev": "bunx nuxi dev --dotenv .config/local.env --no-restart --public --port 3000",
"dev:remote": "bunx nuxi dev --dotenv .config/production.env --dotenv .env --dotenv .env.local --no-restart --public --port 3000",
"build": "NODE_OPTIONS='--max-old-space-size=4096' nuxt build",
"postinstall": "nuxt prepare",
"prettier": "bunx prettier --write .",
"prettier:check": "bunx prettier --check ."
}- Local:
.config/local.env - Production:
.config/production.env+.env.local - Runtime Config:
nuxt.config.tswith public/private keys
Key Variables:
NUXT_PUBLIC_API_BASE_URL=https://api.earth-app.com
NUXT_PUBLIC_CLOUD_BASE_URL=https://cloud.earth-app.com
NUXT_PUBLIC_MAPS_API_KEY=<public-key>
NUXT_TURNSTILE_SECRET_KEY=<secret>
NUXT_PIXABAY_API_KEY=<secret>
NUXT_ADMIN_API_KEY=<secret>@earth-app/ocean: Shared Protocol Buffer typesnuxt: Framework corevue&vue-router: UI framework & routingtailwindcss&@nuxt/ui: Styling system@pinia/nuxt: Store integrationzod: Schema validationluxon: Date/time handlingyoutube-sr: YouTube search integration
@nuxthub/core: Cloudflare deployment integration@nuxtjs/turnstile: Bot protection@nuxtjs/i18n: Internationalization@nuxtjs/google-fonts: Noto Sans font loading@nuxtjs/robots&@nuxtjs/sitemap: SEO toolingnuxt-viewport: Responsive breakpoint utilities@nuxt/image: Image optimization pipelinenuxt-schema-org: Structured metadata generationnuxt-api-shield: Route-level API throttling/protection
17 Iconify sets included via devDependencies:
- Material Design (symbols, symbols-light)
- Lucide, Heroicons, Phosphor
- Carbon, Solar, Game Icons, Health Icons
- And more (see
package.json)
Build Command: NODE_OPTIONS='--max-old-space-size=4096' nuxt build
Output: Cloudflare Workers module
Features:
- Edge-deployed server functions
- Automatic caching via Cloudflare CDN
- Node.js compatibility mode enabled
- Observability at 20% head sampling
Deployment Workflow:
- Code pushed to GitHub (branch:
master) - CI/CD builds Nuxt app
- NuxtHub deploys to Cloudflare Workers
- Static assets served from Cloudflare CDN
Static routes generated at build time:
/about(fully static)/terms-of-service(fully static)/privacy-policy(fully static)/sitemap.xml(SEO)
Dynamic routes use ISR/SWR for on-demand regeneration.
- Cloudflare Turnstile: Bot protection on auth forms
- Server-Side Token Validation: All API requests validated via backend
- CORS Configuration: Allowlisted origins enforced in Nitro server middleware
- Rate Limiting:
nuxt-api-shieldprotection on selected internal API routes + backend 429 handling - Content Security: Admin routes require
ADMINISTRATORaccount type - Environment Secrets: All sensitive keys in runtime config (never client-exposed)
# Install dependencies
bun install
# Run dev server (local environment)
bun run dev
# Run dev server (production environment)
bun run dev:remote
# Format code
bun run prettier
# Check formatting
bun run prettier:checkCreate .config/local.env:
NUXT_PUBLIC_API_BASE_URL=http://localhost:8080
NUXT_PUBLIC_CLOUD_BASE_URL=http://localhost:9000
NUXT_PUBLIC_MAPS_API_KEY=<local-or-dev-key>
NUXT_TURNSTILE_SECRET_KEY=1x00000000000000000000AA
NUXT_PIXABAY_API_KEY=<local-or-dev-key>- Vue components: Instant updates
- Composables: Auto-reload on change
- Server routes: Requires manual restart (use
--no-restartflag cautiously)
# Run type checker
bunx vue-tsc --noEmit- Endpoint:
/api/activity/wikipedia - Purpose: Fetch article summaries for activities
- Response:
WikipediaSummarytype with title, extract, image
- Endpoint:
/api/activity/youtubeSearch - Purpose: Find related videos for activities
- Library:
youtube-srpackage
- Endpoint:
/api/activity/iconSearch - Purpose: Search 50+ icon sets for activity icons
- API: Iconify CDN
- Endpoints:
/api/activity/pixabayImages,/api/activity/pixabayVideos - Purpose: Fetch royalty-free media candidates for activity/event visuals
- Endpoints:
/api/event/geocode,/api/event/autocomplete - Purpose: Geocoding/reverse geocoding and place autocomplete for event locations
All backend requests go through composables:
- Activities →
/v2/activities/* - Users →
/v2/users/* - Prompts →
/v2/prompts/* - Articles →
/v2/articles/* - Events →
/v2/events/*
Authentication Flow:
- User logs in, receives JWT token
- Token is stored/synced through
session_tokencookie + auth store - All requests inject token via
useCurrentSessionToken()/ auth store state - Backend validates token and returns user-specific data
- Mobile-first: Tailwind breakpoints (
sm:,md:,lg:) - Viewport Composables:
nuxt-viewportfor device detection - Adaptive Navigation:
UserDropdownfor mobile, full nav for desktop
- Tailwind Motion: Preset animations (
motion-preset-fade-lg) - Custom CSS: Earth circle rotation animation
- Conditional Rendering:
ClientOnlyfor hydration-sensitive components
- Skeletons:
InfoCardSkeleton,JourneyProgressSkeleton - Async Data:
useAsyncData()handles loading states automatically - Toasts:
useToast()for operation feedback
- Nuxt UI Forms: Integrated with
@nuxt/ui - Zod Validation: Real-time validation in forms
- Error Handling: Toast notifications for API errors
State is managed through Pinia stores plus composable facades:
- Pinia stores: Centralized auth, users, avatars, content, and notifications
- Composables: Domain-specific APIs that orchestrate store actions
- Lifecycle Hooks:
onMounted(),watch(), etc.
Example: User authentication state
export const useAuthStore = defineStore('auth', () => {
const currentUser = ref<User | null | undefined>(undefined);
const sessionToken = ref<string | null>(null);
const fetchCurrentUser = async () => {
/* ... */
};
return { currentUser, sessionToken, fetchCurrentUser };
});- Session Tokens: Persisted in secure cookies and synchronized via
/api/auth/session - Store State: Rehydrated on app mount by auth plugin/composables
- Image Optimization: CDN-hosted assets (
cdn.earth-app.com) - Code Splitting: Automatic per-route chunks
- Tree Shaking: Unused Iconify icons excluded
- Caching Strategy:
- Page rendering: ISR/SWR from 10min to 4hr depending on route
- Internal API cache:
/api/activity/**cached for 1 hour - Client request layer: deduped in-flight requests + bounded in-memory cache
- Static assets: Indefinite (CDN cache)
- Bundle Size: Bun's faster resolution, Tailwind's JIT compilation
Issue: "Cannot find module '@earth-app/ocean'"
Solution: Ensure @earth-app/ocean is installed (bun install);
Ensure a GITHUB_TOKEN is set to install from github packages
Issue: "Turnstile verification failed"
Solution: Check NUXT_TURNSTILE_SECRET_KEY in environment config
Issue: "401 Unauthorized on API requests" Solution: Verify session token is valid, try re-logging in
Issue: "Type errors in TypeScript"
Solution: Run bunx nuxi prepare to regenerate .nuxt/tsconfig.json
- Code Style: Use Prettier (auto-format on commit)
- Commits: Follow conventional commits (
feat:,fix:,docs:) - Type Safety: All new code must be fully typed
- Testing: Manual testing required (automated tests TBD)
- Documentation: Update README for significant changes
- The package is prepared for publishing as a library. A
prepackscript copiesnuxt.config.library.tsinto place before packaging, andpostpackrestores the working tree. Releases are published to GitHub Packages as configured inpackage.json(publishConfig). The CIbuild.ymldemonstrates the canonical publish flow (install, postinstall, bump version, publish).
Local packaging tips:
- Use
bun run postinstallafterbun installto ensure native hooks are prepared before building or publishing. - When preparing a package release, prefer running the CI workflow or following the steps in
.github/workflows/build.ymlto replicate the published artifact.
- This frontend works closely with upstream backend repositories. Two commonly referenced backends are:
earth-app/mantle2— core API and business logic (user, activities, prompts, articles)earth-app/cloud— Cloud/edge services and worker-side integrations used by NuxtHub/Cloudflare deployments
When updating API surface (paths, response schemas), coordinate changes with the backend repos and update the local Zod schemas in src/shared/utils/schemas.ts and the TypeScript types if shared contracts change.
See LICENSE file for details.
- Framework: Nuxt by the Nuxt team
- UI Components: @nuxt/ui
- Icons: Iconify
- Deployment: NuxtHub + Cloudflare
- Developed by: Gregory Mitchell
For questions or support, open an issue on GitHub or contact the development team.