mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
- Introduced Vite as the new build tool for the web frontend, enhancing performance and development speed. - Added essential components including landing, login, and admin pages. - Implemented Zustand for state management and integrated error boundaries for improved error handling. - Established a unified API client and validation schemas for consistent data handling. - Created comprehensive test setups and initial tests for core functionalities.
14 KiB
14 KiB
🏗️ Scalable Vite Architecture for Omnivore Multi-Platform
🎯 Executive Summary
Design a modular, extensible Vite architecture that supports multiple web interfaces while maintaining compatibility with mobile apps and browser extensions. This architecture enables rapid development while providing a foundation for future micro-frontend scaling.
📊 Current Multi-Platform Architecture Analysis
Current Client Landscape
| Platform | Technology | API Communication | Current State |
|---|---|---|---|
| Web App | Next.js + React | GraphQL + REST | Monolithic, slow builds |
| iOS App | SwiftUI + Swift | GraphQL + REST | Native, Apollo Client |
| Android App | Kotlin + Compose | GraphQL + REST | Native, Apollo Client |
| Browser Extension | Vanilla JS | GraphQL + REST | Content scripts, API calls |
| Safari Extension | Swift + JS | GraphQL + REST | Native messaging |
Current API Communication Patterns
// All clients use similar patterns:
// 1. GraphQL for data fetching
// 2. REST for authentication
// 3. JWT tokens for auth
// 4. Same backend endpoints
// Web (Next.js)
const { data } = useSWR([query, variables], makeGqlFetcher(query, variables))
// Mobile (iOS/Android)
let apolloClient = ApolloClient.Builder()
.serverUrl(serverUrl())
.addHttpHeader('Authorization', authToken())
.build()
// Extension (JavaScript)
fetch(url, {
method: 'POST',
headers: { Authorization: apiKey, 'Content-Type': 'application/json' },
body: JSON.stringify(query),
})
🚀 Proposed Vite Architecture
1. Modular Package Structure
packages/
├── web-vite/ # Main Vite application
│ ├── src/
│ │ ├── apps/ # Different web interfaces
│ │ │ ├── main/ # Primary web app
│ │ │ ├── reader/ # Standalone reader
│ │ │ ├── admin/ # Admin interface
│ │ │ └── embed/ # Embedded widgets
│ │ ├── shared/ # Shared components & utilities
│ │ │ ├── components/ # Reusable UI components
│ │ │ ├── hooks/ # Custom React hooks
│ │ │ ├── services/ # API services
│ │ │ ├── stores/ # State management
│ │ │ └── types/ # TypeScript definitions
│ │ └── lib/ # Core libraries
│ ├── vite.config.ts # Vite configuration
│ └── package.json
├── shared/ # Cross-platform shared code
│ ├── api-client/ # GraphQL client
│ ├── types/ # Shared TypeScript types
│ ├── utils/ # Utility functions
│ └── constants/ # Shared constants
└── web/ # Legacy Next.js (during migration)
2. Multi-App Vite Configuration
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { resolve } from 'path'
export default defineConfig(({ command, mode }) => {
const app = process.env.VITE_APP || 'main'
return {
plugins: [react()],
root: `src/apps/${app}`,
build: {
outDir: `../../dist/${app}`,
rollupOptions: {
input: {
main: resolve(__dirname, `src/apps/${app}/index.html`),
},
},
},
resolve: {
alias: {
'@shared': resolve(__dirname, 'src/shared'),
'@components': resolve(__dirname, 'src/shared/components'),
'@services': resolve(__dirname, 'src/shared/services'),
'@types': resolve(__dirname, 'src/shared/types'),
'@utils': resolve(__dirname, 'src/shared/utils'),
},
},
server: {
port: getPortForApp(app),
proxy: {
'/api': 'http://localhost:4001',
'/graphql': 'http://localhost:4001',
},
},
}
})
function getPortForApp(app: string): number {
const ports = {
main: 3000,
reader: 3001,
admin: 3002,
embed: 3003,
}
return ports[app] || 3000
}
3. Shared API Client Architecture
// packages/shared/api-client/src/omnivore-client.ts
import { ApolloClient, InMemoryCache, createHttpLink } from '@apollo/client'
import { setContext } from '@apollo/client/link/context'
export interface OmnivoreClientConfig {
baseUrl: string
platform: 'web' | 'mobile' | 'extension'
authToken?: string
}
export class OmnivoreClient {
private apolloClient: ApolloClient<any>
constructor(config: OmnivoreClientConfig) {
const httpLink = createHttpLink({
uri: `${config.baseUrl}/api/graphql`,
})
const authLink = setContext((_, { headers }) => {
const token = config.authToken || this.getStoredToken()
return {
headers: {
...headers,
'X-OmnivoreClient': config.platform,
...(token && { Authorization: `Bearer ${token}` }),
},
}
})
this.apolloClient = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache({
typePolicies: {
// Platform-specific cache policies
Query: {
fields: {
libraryItems: {
merge: config.platform === 'mobile' ? false : true,
},
},
},
},
}),
})
}
// Platform-specific token storage
private getStoredToken(): string | null {
if (typeof window !== 'undefined') {
return localStorage.getItem('authToken')
}
return null
}
getApolloClient() {
return this.apolloClient
}
}
// Platform-specific implementations
export const createWebClient = (baseUrl: string) =>
new OmnivoreClient({ baseUrl, platform: 'web' })
export const createMobileClient = (baseUrl: string, authToken: string) =>
new OmnivoreClient({ baseUrl, platform: 'mobile', authToken })
export const createExtensionClient = (baseUrl: string) =>
new OmnivoreClient({ baseUrl, platform: 'extension' })
4. Shared Component Library
// packages/shared/components/src/index.ts
export { Button } from './Button'
export { Input } from './Input'
export { Modal } from './Modal'
export { ArticleCard } from './ArticleCard'
export { LibraryGrid } from './LibraryGrid'
export { ReaderView } from './ReaderView'
// Platform-specific variants
export { MobileArticleCard } from './variants/MobileArticleCard'
export { WebArticleCard } from './variants/WebArticleCard'
export { ExtensionArticleCard } from './variants/ExtensionArticleCard'
// packages/shared/components/src/ArticleCard/index.tsx
import React from 'react'
import { Article } from '@types/article'
import { WebArticleCard } from './variants/WebArticleCard'
import { MobileArticleCard } from './variants/MobileArticleCard'
import { ExtensionArticleCard } from './variants/ExtensionArticleCard'
interface ArticleCardProps {
article: Article
platform?: 'web' | 'mobile' | 'extension'
variant?: 'default' | 'compact' | 'detailed'
}
export const ArticleCard: React.FC<ArticleCardProps> = ({
article,
platform = 'web',
variant = 'default',
}) => {
const Component = {
web: WebArticleCard,
mobile: MobileArticleCard,
extension: ExtensionArticleCard,
}[platform]
return <Component article={article} variant={variant} />
}
5. State Management Architecture
// packages/shared/stores/src/auth-store.ts
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface AuthState {
user: User | null
token: string | null
isAuthenticated: boolean
login: (email: string, password: string) => Promise<void>
logout: () => void
setToken: (token: string) => void
}
export const useAuthStore = create<AuthState>()(
persist(
(set, get) => ({
user: null,
token: null,
isAuthenticated: false,
login: async (email: string, password: string) => {
const response = await fetch('/api/v2/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
})
const data = await response.json()
if (data.success) {
set({
user: data.user,
token: data.accessToken,
isAuthenticated: true,
})
}
},
logout: () => {
set({ user: null, token: null, isAuthenticated: false })
},
setToken: (token: string) => {
set({ token, isAuthenticated: !!token })
},
}),
{
name: 'omnivore-auth',
// Platform-specific storage
storage: typeof window !== 'undefined' ? localStorage : undefined,
}
)
)
🔄 Migration Strategy
Phase 1: Foundation (Week 1)
# Day 1-2: Setup
mkdir packages/web-vite
cd packages/web-vite
npm create vite@latest . -- --template react-ts
# Install shared dependencies
npm install @tanstack/react-query @apollo/client zustand
npm install @radix-ui/react-* @stitches/react
# Day 3-4: Core Architecture
# - Set up shared package structure
# - Create API client
# - Implement auth store
# - Create basic routing
Phase 2: App Migration (Week 2)
# Day 1-3: Main App Migration
# - Migrate authentication pages
# - Migrate library management
# - Migrate article reading
# Day 4-5: Additional Apps
# - Create standalone reader app
# - Create admin interface
# - Create embed widgets
Phase 3: Optimization (Week 3)
# Day 1-2: Performance
# - Bundle optimization
# - Code splitting
# - Lazy loading
# Day 3-5: Testing & Deployment
# - Unit tests
# - E2E tests
# - Production deployment
🌐 Multi-App Deployment Strategy
1. Development Environment
# docker-compose.dev.yml
services:
web-vite-main:
build: ./packages/web-vite
ports: ['3000:3000']
environment:
- VITE_APP=main
- VITE_API_URL=http://localhost:4001
command: npm run dev:main
web-vite-reader:
build: ./packages/web-vite
ports: ['3001:3001']
environment:
- VITE_APP=reader
- VITE_API_URL=http://localhost:4001
command: npm run dev:reader
web-vite-admin:
build: ./packages/web-vite
ports: ['3002:3002']
environment:
- VITE_APP=admin
- VITE_API_URL=http://localhost:4001
command: npm run dev:admin
2. Production Deployment
// nginx.conf
server {
listen 80;
server_name omnivore.app;
# Main app
location / {
root /var/www/omnivore/main;
try_files $uri $uri/ /index.html;
}
# Reader app
location /reader {
root /var/www/omnivore/reader;
try_files $uri $uri/ /index.html;
}
# Admin app
location /admin {
root /var/www/omnivore/admin;
try_files $uri $uri/ /index.html;
}
# API proxy
location /api {
proxy_pass http://api-nest:4001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
📱 Cross-Platform Compatibility
1. Shared API Client Usage
// Web App
import { createWebClient } from '@shared/api-client'
const client = createWebClient('https://api.omnivore.app')
// Mobile App (React Native)
import { createMobileClient } from '@shared/api-client'
const client = createMobileClient('https://api.omnivore.app', authToken)
// Extension
import { createExtensionClient } from '@shared/api-client'
const client = createExtensionClient('https://api.omnivore.app')
2. Component Reuse
// Shared components work across platforms
import { ArticleCard, Button, Modal } from '@shared/components'
// Platform-specific rendering
;<ArticleCard article={article} platform="web" variant="detailed" />
3. State Synchronization
// Shared state stores work across web apps
import { useAuthStore, useLibraryStore } from '@shared/stores'
// All web apps share the same auth state
const { user, isAuthenticated } = useAuthStore()
🚀 Future Micro-Frontend Evolution
Phase 1: Modular Apps (Current)
- Multiple Vite apps sharing components
- Shared API client and state management
- Independent deployment per app
Phase 2: Module Federation (Future)
// webpack.config.js
const ModuleFederationPlugin = require('@module-federation/webpack')
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'omnivore_shell',
remotes: {
auth: 'auth@http://localhost:3001/remoteEntry.js',
library: 'library@http://localhost:3002/remoteEntry.js',
reader: 'reader@http://localhost:3003/remoteEntry.js',
},
shared: {
react: { singleton: true },
'@tanstack/react-query': { singleton: true },
},
}),
],
}
Phase 3: Full Micro-Frontends (Long-term)
- Independent teams per micro-frontend
- Technology flexibility (React, Vue, Angular)
- Independent deployment and scaling
📊 Benefits of This Architecture
Immediate Benefits
- ✅ 50-100x faster development (Vite vs Next.js)
- ✅ Modular apps (main, reader, admin, embed)
- ✅ Shared components across web interfaces
- ✅ Consistent API client across platforms
- ✅ Independent deployment per app
Scalability Benefits
- ✅ Team autonomy (different teams can own different apps)
- ✅ Technology flexibility (can mix React, Vue, Angular)
- ✅ Independent scaling (scale reader separately from main app)
- ✅ Micro-frontend ready (easy migration path)
Cross-Platform Benefits
- ✅ Shared code between web, mobile, extension
- ✅ Consistent UX across all platforms
- ✅ Unified API client with platform-specific optimizations
- ✅ State synchronization across web apps
🎯 Implementation Priority
Week 1: Foundation
- Set up Vite with multi-app configuration
- Create shared API client
- Implement auth store and routing
- Migrate authentication pages
Week 2: Core Apps
- Migrate main web app (library management)
- Create standalone reader app
- Create admin interface
- Implement shared components
Week 3: Polish & Deploy
- Performance optimization
- Testing and quality assurance
- Production deployment
- Documentation and handoff
This architecture provides immediate performance gains while establishing a scalable foundation for future growth and team expansion.