📚 Introduction to TSStarter Universal Architecture
📚 Introduction to TSStarter Universal Architecture
Build applications that work across web, mobile, and desktop platforms with TSStarter's Web + API Gateway architecture
🎯 Universal Apps - Multi-Platform Architecture
TSStarter's Universal Apps architecture enables you to build applications that work seamlessly across multiple platforms:
- 🌐 Web Application - Full-featured web app with SSR and real-time capabilities
- 📱 Mobile Apps - Native iOS/Android apps using the REST API
- 🖥️ Desktop Apps - Cross-platform desktop applications
- 🔌 API Gateway - Unified REST API for all external clients
🏗️ Architecture Overview
The Universal Apps architecture separates concerns into two main components:
Web App (TanStack Start)
- Internal RPC Mode - Server-side uses direct TypeScript calls (no HTTP overhead)
- TanStack Start Server Routes - Client-side RPC uses TanStack Start's file-based routing via
/api/rpc/$ - Client-side RPC - Browser uses RPC over HTTP through TanStack Start Server Route handler
- SSR - Server-side rendering for optimal performance
- Real-time - Live updates via Server-Sent Events
- Port: 3000 (development) / Vercel (production)
API Gateway (Bun + ElysiaJS)
- REST API - Standard REST endpoints for external clients
- OpenAPI - Auto-generated OpenAPI documentation
- Type-safe - Shared business logic with Web app
- Port: 3001 (development) / Railway (production)
🔄 How It Works
┌─────────────────────────────────────────────────────────┐
│ Web App (TanStack Start) - Internal oRPC │
│ │
│ Server-side (SSR/Server Loaders): │
│ createRouterClient(serverRouter) │
│ ↓ Direct TS calls (no HTTP) │
│ oRPC Router → DB/Auth │
│ │
│ Client-side (Browser): │
│ RPCLink → /api/rpc/$ → Start Server Route │
│ → RPCHandler → Shared Routers → DB/Auth │
│ │
└─────────────────────────────────────────────────────────┘
│
│ Shared Routers (oRPC)
│
┌─────────────────────────────────────────────────────────┐
│ API Gateway (Bun + ElysiaJS) - REST ONLY │
│ │
│ Mobile/Desktop Clients: │
│ REST → /api/rest/* → REST Adapter → Shared Routers │
│ │
│ Endpoints: │
│ - GET /api/rest/tasks │
│ - POST /api/rest/tasks │
│ - GET /api/rest/tasks/:id │
│ - GET /api/openapi.json │
│ - GET /api/docs (Scalar UI) │
│ │
└─────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────┐
│ oRPC Shared Routers │
│ services/shared/ │
│ routers/ │
│ - user │
│ - newsletter │
│ - realtime │
│ - task │
│ - ai │
│ - comments │
└───────────┬───────────┘
│
▼
┌───────────────────────┐
│ Database (Drizzle) │
│ Auth (Better Auth) │
└───────────────────────┘
🎯 Key Benefits
1. Code Reuse
- Shared Business Logic - Same routers work for both Web and API Gateway
- Type Safety - End-to-end TypeScript across all platforms
- Single Source of Truth - One codebase, multiple entry points
2. Platform Optimization
- Web: Fast internal RPC calls (no HTTP overhead on server-side)
- Mobile/Desktop: Standard REST API (works with any HTTP client)
- Flexibility: Choose the right protocol for each platform
3. Developer Experience
- Type-Safe Clients - Auto-generated clients for TypeScript, Swift, Kotlin, C#
- OpenAPI Documentation - Interactive API docs at
/api/docs - Hot Reload - Fast development with instant updates
🛠️ Technology Stack
| Component | Technology | Purpose |
|---|---|---|
| 🌐 Web App | TanStack Start + React 19 + Vite | Full-stack web framework |
| 🔌 API Gateway | Bun + ElysiaJS | High-performance HTTP server |
| 📦 Monorepo | Bun | Fast package manager & runtime |
| 🔗 RPC | oRPC | Type-safe RPC services |
| 🗄️ Database | SQLite (dev) / Turso (prod) | Distributed SQL database |
| 🔐 Auth | Better Auth | Authentication & authorization |
| 📝 API Docs | OpenAPI + Scalar UI | Interactive API documentation |
| 🚀 Deployment | Vercel (Web) + Railway (API) | Platform-optimized hosting |
📋 Project Structure
Note: This project uses a Bun monorepo structure, leveraging Bun's fast package manager and runtime for optimal development experience across all services.
tsstarter/
├── 📁 apps/
│ ├── 📁 web/ # Web Application
│ │ ├── src/
│ │ │ ├── routes/ # TanStack Start routes
│ │ │ ├── components/ # React components
│ │ │ └── lib/ # Utilities & RPC client
│ │ └── content/ # MDX content (docs/blog)
│ │
│ └── 📁 api/ # API Gateway
│ ├── src/
│ │ ├── router.ts # Combined oRPC router
│ │ ├── rest-adapter.ts # REST → oRPC mapping
│ │ ├── openapi.ts # OpenAPI spec generation
│ │ └── server.ts # ElysiaJS server
│ └── scripts/
│ └── generate-clients.ts # Client code generation
│
├── 📁 services/
│ ├── 📁 shared/ # Shared Business Logic
│ │ ├── routers/ # Framework-agnostic routers
│ │ │ ├── user.ts # User management
│ │ │ ├── newsletter.ts # Newsletter subscriptions
│ │ │ ├── realtime.ts # Real-time updates
│ │ │ ├── task.ts # Task management
│ │ │ ├── ai.ts # AI features
│ │ │ └── comments.ts # Comments system
│ │ ├── router.ts # Router factory
│ │ └── events.ts # Event publisher
│ │
│ └── 📁 web/ # Web App Router Composition
│ ├── routes/
│ │ └── index.ts # Combines shared routers for web
│ ├── orpc.ts # oRPC server setup
│ └── handler.ts # Request handler
│
└── 📁 packages/
└── 📁 db/ # Database layer
├── schema.ts # Drizzle schema
└── migrations/ # Database migrations
🔄 Migration from Web-Only to Universal Apps
TSStarter originally started as a web-only application with all business logic embedded in the web app. We migrated to the Universal Apps architecture to support multiple platforms while maintaining code reuse and type safety.
Before: Web-Only Architecture
The original architecture had everything in one place:
src/
├── server/routes/ # oRPC procedures (TanStack Start specific)
├── routes/ # TanStack Start routes
└── components/ # React components
Characteristics:
- Single web application
- Routers tightly coupled to TanStack Start
- No way to expose APIs for mobile/desktop clients
- All logic in one codebase
After: Universal Apps Architecture
The new architecture separates concerns and enables multi-platform support:
services/shared/routers/ # Framework-agnostic business logic
apps/web/ # Web app (uses shared routers)
apps/api/ # API Gateway (uses shared routers)
Key Changes:
- Extracted Routers - Moved all business logic to
services/shared/routers/ - Made Routers Framework-Agnostic - Routers now work with any oRPC-compatible framework
- Created API Gateway - New service that exposes REST endpoints
- Router Composition - Both Web and API Gateway compose the same shared routers
Migration Process
-
Refactored Routers - Converted TanStack Start-specific routers to framework-agnostic ones
- Removed framework dependencies
- Made routers accept dependencies as parameters
- Used router factory pattern for consistency
-
Created Shared Package - Established
services/shared/for reusable business logic- All routers moved here
- Router factory pattern implemented
- Event publisher for real-time updates
-
Updated Web App - Modified web app to use shared routers
services/web/routes/index.tsnow imports and composes shared routers- Maintains same functionality with better structure
-
Built API Gateway - Created new service for external clients
- REST adapter maps REST → oRPC procedures
- OpenAPI spec generation
- Scalar UI for documentation
Benefits of Migration
- Multi-Platform Support - Same codebase works for web, mobile, and desktop
- Code Reuse - Business logic defined once, used everywhere
- Independent Deployment - Web and API Gateway can be deployed separately
- Better Performance - Co-locate databases with services
- Type Safety Maintained - Full TypeScript support across all platforms
- Future-Proof - Easy to add new platforms (mobile apps, desktop apps)
Backward Compatibility
The web app maintains 100% backward compatibility:
- All existing features work the same
- Same API endpoints for web clients
- No breaking changes for web users
- Internal RPC mode still provides optimal performance
🛣️ TanStack Start Server Routes
The Web App uses TanStack Start's file-based API routing to handle client-side RPC requests. This provides type-safe, framework-integrated API endpoints.
How It Works
File-Based Routing (apps/web/src/routes/api/rpc.$.ts):
- TanStack Start uses file-based routing for API endpoints
- The
$parameter is a catch-all that handles all RPC procedure paths - Route defined as
/api/rpc/$matches all requests like/api/rpc/task.list,/api/rpc/user.getProfile, etc.
Request Flow:
Browser Client
↓
RPCLink (oRPC client)
↓
HTTP Request → /api/rpc/task.list
↓
TanStack Start Server Route (/api/rpc/$)
├─ rpc.$.ts (handle function)
├─ Gets session from Better Auth
└─ Calls serverHandler.handle(request, { context, prefix: "/api/rpc" })
↓
serverHandler (RPCHandler from services/web/handler.ts)
├─ Processes request through oRPC
└─ Routes to appropriate procedure
↓
serverRouter (from services/web/routes/index.ts)
├─ Composed from shared routers
└─ Contains all oRPC procedures
↓
Shared Router (services/shared/routers/task.ts)
├─ Business logic execution
└─ Database operations via Drizzle
↓
Database/Auth (Turso/SQLite + Better Auth)
Key Benefits
- Type-Safe Routing - File-based routes are type-checked at build time
- Framework Integration - Seamlessly integrated with TanStack Start's routing system
- SSR Support - Works with server-side rendering and client-side navigation
- Error Handling - Built-in error boundaries and error handling
- Request Context - Full access to request headers, cookies, and session data
Server-Side vs Client-Side
Server-Side (SSR/Server Loaders):
- Uses
createRouterClient(serverRouter)- Direct TypeScript calls - No HTTP overhead - Fastest possible performance
- Used in server loaders, server functions, and SSR
What are Server Loaders? Server Loaders are functions defined in route configurations that run on the server before rendering a page. According to the TanStack Start documentation, loaders are part of TanStack Router's route definition system and execute during SSR to fetch data that the page needs:
// Example: Server Loader in a route
export const Route = createFileRoute("/docs/")({
loader: async () => {
// This runs on the server during SSR
// Can use createRouterClient for direct TS calls (no HTTP)
const data = await serverRouter.task.list({ userId: "123" });
return { tasks: data };
},
component: DocsPage,
});
// In the component, access loader data:
function DocsPage() {
const { tasks } = Route.useLoaderData();
// Use tasks in your component
}
How Loaders Work:
- Server-Side Execution - Loaders run on the server before the page renders
- Data Fetching - They can call server functions, database queries, or API endpoints
- Direct TypeScript Calls - Can use
createRouterClient()for zero HTTP overhead - Data Access - Component accesses data via
Route.useLoaderData() - Automatic Revalidation - Use
router.invalidate()to refresh data after mutations
Note: Server Loaders are different from:
- Server Functions (
createServerFn) - Standalone functions that can be called from the client - Static Server Functions - Server functions that execute at build time and cache results (see TanStack Start docs)
Server Loaders are specifically part of route definitions and run automatically during route rendering, making them perfect for data fetching that needs to happen before the page renders.
Benefits of Server Loaders:
- Runs on Server - Data fetching happens before page render
- Direct TypeScript Calls - Uses
createRouterClient()for zero HTTP overhead - SSR Support - Data is available immediately when page loads
- Type-Safe - Full TypeScript support with automatic type inference
- SEO Friendly - Content is available for search engines
Client-Side (Browser):
- Uses
RPCLink→/api/rpc/$→ TanStack Start Server Route - HTTP-based RPC calls
- Full type safety maintained through oRPC
🏛️ Router Architecture
The Universal Apps architecture uses a shared router pattern where all business logic is defined once and reused across platforms:
Shared Routers (services/shared/routers/)
All business logic routers are framework-agnostic and located in services/shared/routers/:
user.ts- User management (profile, authentication)newsletter.ts- Newsletter subscriptionsrealtime.ts- Real-time updates via Event Publishertask.ts- Task management (CRUD operations)ai.ts- AI-powered features (chat, suggestions)comments.ts- Comments system
These routers are framework-agnostic and work with any oRPC-compatible framework.
Router Composition
Web App (services/web/routes/index.ts):
- Imports shared routers from
services/shared/routers/ - Combines them using the router factory pattern
- Used by TanStack Start for server-side and client-side RPC
API Gateway (apps/api/src/router.ts):
- Imports the same shared routers
- Combines them into a unified router
- Exposed via REST adapter at
/api/rest/*
Benefits
- Single Source of Truth - Business logic defined once
- Type Safety - End-to-end TypeScript across all platforms
- Consistency - Same behavior on Web and API Gateway
- Maintainability - Update logic once, works everywhere
🔌 API Gateway Endpoints
REST API
All REST endpoints follow standard HTTP conventions:
GET /api/rest/tasks- List all tasksPOST /api/rest/tasks- Create a new taskGET /api/rest/tasks/:id- Get a specific taskPUT /api/rest/tasks/:id- Update a taskDELETE /api/rest/tasks/:id- Delete a task
OpenAPI Documentation
GET /api/docs- Interactive Scalar UI (modern API documentation)GET /api/openapi.json- OpenAPI 3.0 specification
Health & Info
GET /- API informationGET /health- Health check endpoint
🚀 Client Code Generation
Generate type-safe TypeScript client:
# Generate TypeScript client
bun run generate:clients typescript
Generated client provides:
- Full type safety - TypeScript definitions for all endpoints
- Request/Response types - Auto-generated from OpenAPI spec
- Error handling - Type-safe error responses
- Authentication - Built-in auth token handling
🔐 Authentication
Both Web and API Gateway use the same authentication system:
- Better Auth - Modern authentication framework
- Session-based - Secure cookie-based sessions
- OAuth Support - GitHub, Google, and more
- API Keys - For programmatic access (mobile/desktop)
Web App Authentication
- Server-side: Direct session access
- Client-side: Automatic cookie handling
API Gateway Authentication
- REST: Bearer token in
Authorizationheader - Scalar UI: Interactive auth in API documentation interface
📊 Database Architecture
Development
- SQLite - Local file-based database
- Drizzle Studio - Visual database management
Production
- Turso: Cloud database (great for Vercel & Railway)
- Shared Schema - Same Drizzle schema for web and API gateway
🎨 Development Workflow
1. Local Development
# Start Web app (port 3000)
bun run dev
# Start API Gateway (port 3001)
cd apps/api
bun run dev
2. Testing REST API
- Visit
http://localhost:3001/api/docsfor Scalar UI (interactive API documentation) - Test endpoints interactively with authentication
- View OpenAPI spec at
/api/openapi.json
3. Client Development
- Generate clients:
bun run generate:clients - Import generated clients in your mobile/desktop app
- Use type-safe API calls
🚀 Deployment
Web App → Vercel
- Automatic deployments from Git
- Edge functions for optimal performance
- Co-located with Turso database
API Gateway → Railway
- Independent deployment pipeline
- Separate environment variables
- Co-located with Turso database
Environment Variables
- Web:
DATABASE_URL,DATABASE_AUTH_TOKEN(Vercel) - API:
DATABASE_URL,DATABASE_AUTH_TOKEN(Railway) - Both:
BETTER_AUTH_SECRET,BETTER_AUTH_URL
📚 Next Steps
Ready to build your Universal App? Follow these guides:
- Setup Guide → - Set up both Web and API Gateway
- Deployment Guide → - Deploy to production
- API Documentation → - Complete API reference
TSStarter is crafted with the best TypeScript tools for building web and universal applications.