Back to Eliza

Technical Implementation

packages/feed/docs/planning/mobile/03-implementation.md

2.0.36.0 KB
Original Source

Technical Implementation

Static Export

Setting output: 'export' in next.config.ts outputs static HTML/CSS/JS files. No Node.js server runs at runtime. Required because Capacitor loads files from the device's local filesystem.

What Breaks in Static Export

FeatureUsed in Feed?ImpactMitigation
API Routes✅ 315 routesNot included in static exportAPI stays on Vercel; mobile calls remote API
Middleware✅ CORS, auth gatingNot availableCORS: add Capacitor origins. Auth gating: client-side.
Server Actions✅ 3 filesCannot call server functionsConverted to API routes ✅
headers() / cookies()✅ layout, home pageNot available at runtimeRemoved from mobile pages ✅
next/image optimization✅ 15 filesVercel Image CDN unavailableCustom loader ✅
redirect() (server)✅ 5 pagesServer redirect unavailableClient-side useRouter().replace()
generateMetadata() with DB✅ 2 pagesDynamic OG tags impossibleExcluded from mobile ✅
force-dynamic export✅ feed/agents layoutsIncompatibleRemoved directive ✅
Dynamic routes✅ 15 routesRequire generateStaticParamsServer/client split with placeholder params ✅

Dynamic Routes Solution

Each dynamic route uses a server/client split:

  • page.tsx — server component with generateStaticParams returning placeholder params
  • client.tsx — the actual UI with useParams()

In-app navigation uses client-side pushState (no file lookup). Deep links are intercepted by appUrlOpen before the WebView resolves a file. The only edge case is page refresh on a dynamic route — the placeholder HTML file serves as a shell.


Code Sharing Strategy

The mobile app shares code from apps/web/src/ via webpack aliases:

typescript
// apps/mobile/next.config.ts
config.resolve.alias = {
  '@/components': path.join(webSrc, 'components'),
  '@/hooks': path.join(webSrc, 'hooks'),
  '@/stores': path.join(webSrc, 'stores'),
  '@/utils': path.join(webSrc, 'utils'),
  '@/contexts': path.join(webSrc, 'contexts'),
  '@/lib': path.join(webSrc, 'lib'),
  '@/types': path.join(webSrc, 'types'),
  '@web': webSrc,        // for importing web page components
  '@/mobile': mobileSrc, // for mobile-specific code
};

@/app/ is NOT aliased — the mobile app has its own page layer. For importing web page components (re-exports), use @web/app/... which doesn't trigger Next.js route discovery.


Key Technical Decisions

apiUrl() utility

Every fetch('/api/...') call goes through apiUrl() (apps/web/src/utils/api-url.ts). When NEXT_PUBLIC_API_URL is unset (web), it's a no-op. When set (mobile), it prepends the base URL. This was applied to ~190 fetch call sites across ~150 files, including apiFetch(), useSSE, SSEManager, custom wrappers (callApi, apiCall), and direct fetch calls.

Server actions → API routes

3 server action files (_actions/onchain.ts, _actions/nft.ts, _actions/utils.ts) were replaced with API routes:

  • POST /api/onchain — handles buy-shares, sell-shares, update-agent-profile via Privy sponsored transactions
  • POST /api/nft/mint/execute — full mint flow (prepare → send tx → poll for confirmation with exponential backoff)
  • GET /api/profiles/resolve/[identifier] — resolves ambiguous identifiers to canonical profile paths

The 3 calling hooks were rewritten to use fetch(apiUrl(...)) instead of direct server action imports. Minor web perf regression (~1-5ms) but eliminates the @/app/_actions import dependency that would break webpack aliases.

Device tokens

Push notification device tokens are stored in Redis (not a DB table) — they're ephemeral data that changes when users reinstall. Redis hash per user, 90-day TTL, supports multiple devices per user.

Native features

All Capacitor plugins are lazy-loaded via dynamic import(). This means native feature code is never bundled on web, and native calls are no-ops when Capacitor.isNativePlatform() returns false.


File Manifest

New Files Created

FilePurpose
apps/web/src/utils/api-url.tsapiUrl() utility
apps/web/src/app/api/onchain/route.tsOn-chain transaction API
apps/web/src/app/api/nft/mint/execute/route.tsNFT mint execute API
apps/web/src/app/api/profiles/resolve/[identifier]/route.tsProfile resolution API
apps/web/src/app/api/notifications/register-device/route.tsPush token registration
apps/web/src/app/api/notifications/unregister-device/route.tsPush token removal
apps/web/public/.well-known/apple-app-site-associationiOS Universal Links
apps/web/public/.well-known/assetlinks.jsonAndroid App Links
apps/mobile/Complete mobile app (77+ files)
apps/mobile/src/components/AppUrlListener.tsxPrivy OAuth handler
apps/mobile/src/lib/platform.tsPlatform detection
apps/mobile/src/lib/haptics.tsHaptic feedback
apps/mobile/src/lib/push-notifications.tsPush setup
apps/mobile/src/lib/status-bar.tsStatus bar theming
apps/mobile/src/lib/deep-links.tsApp lifecycle
apps/mobile/src/lib/native-init.tsNative init orchestration
apps/mobile/src/lib/image-loader.tsCustom image loader
packages/testing/unit/mobile/46 unit tests

Modified Files

~150 files updated with apiUrl() for fetch calls, plus SSE fixes, shared code moves, hook rewrites, Providers update, and middleware CORS update.


Remote URL Alternative

Instead of static export, Capacitor can load from a remote URL:

typescript
const config: CapacitorConfig = {
  server: { url: 'https://play.feed.market' },
};

Pros: Zero code changes. Full feature parity. Instant updates. Cons: Requires internet. Slower load. Higher Apple rejection risk. Use for: Dev testing, Android Play Store (less strict), internal TestFlight.