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
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.