This document details the technology choices for PairUX and the rationale behind each decision.
Choice: pnpm v8+
Rationale:
- Efficient disk space usage via content-addressable storage
- Strict dependency resolution prevents phantom dependencies
- Native workspace support
- Faster than npm/yarn for monorepos
- Works well with Turborepo
Alternatives Considered:
| Tool | Rejected Because |
|---|---|
| npm | Slower, no workspace hoisting control |
| yarn | Berry complexity, pnpm is faster |
Choice: Turborepo v2+
Rationale:
- Incremental builds with caching
- Parallel task execution
- Simple configuration
- Works seamlessly with pnpm workspaces
- Remote caching available (optional)
Configuration (turbo.json):
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"lint": {},
"test": {
"dependsOn": ["build"]
}
}
}Alternatives Considered:
| Tool | Rejected Because |
|---|---|
| Nx | More complex, overkill for this project size |
| Lerna | Deprecated in favor of native workspace tools |
| Rush | Microsoft-specific, steeper learning curve |
Choice: Electron v28+ (latest stable)
Rationale:
- Required for native screen capture APIs
- Required for system-level input injection
- Cross-platform (macOS, Windows, Linux)
- Mature ecosystem
- Good TypeScript support
- Built-in auto-updater
Key Electron APIs Used:
desktopCapturer- Screen/window capturesystemPreferences- Permission checksglobalShortcut- Emergency revoke hotkeyTray- System tray integrationsafeStorage- Secure credential storageautoUpdater- App updates
Alternatives Considered:
| Tool | Rejected Because |
|---|---|
| Tauri | No mature screen capture API, Rust complexity |
| NW.js | Less active development, smaller ecosystem |
| Flutter Desktop | No native screen capture, immature |
Choice: nut.js v4+
Rationale:
- Cross-platform mouse/keyboard automation
- Active maintenance
- TypeScript support
- Handles screen coordinate translation
- Supports all required input types
Capabilities:
import { mouse, keyboard, screen } from '@nut-tree/nut-js';
// Mouse control
await mouse.move([{ x: 100, y: 200 }]);
await mouse.click(Button.LEFT);
await mouse.scroll(0, -5); // Scroll up
// Keyboard control
await keyboard.type('Hello');
await keyboard.pressKey(Key.Enter);
await keyboard.releaseKey(Key.Enter);Alternatives Considered:
| Tool | Rejected Because |
|---|---|
| robotjs | Unmaintained, build issues |
| node-key-sender | Windows only |
| Native modules | Too much platform-specific code |
Choice: React 18 + Vite for Electron renderer
Rationale:
- Fast HMR during development
- Modern build tooling
- Consistent with web app (code sharing)
- Large ecosystem
Note: The PRD mentions React 16.2.1 for the web app. We'll use React 18 for the desktop app since it's isolated, but can downgrade if strict consistency is required.
Choice: Next.js 16.2 (latest stable)
Rationale:
- Server-side rendering for SEO
- App Router for modern patterns
- API routes if needed
- Image optimization
- Built-in performance optimizations
- Vercel deployment integration
Key Features Used:
- App Router (
/appdirectory) - Server Components (marketing pages)
- Client Components (viewer UI)
- Metadata API (SEO)
- Static generation (marketing pages)
Choice: Tailwind CSS v3+
Rationale:
- Utility-first approach
- Consistent design system
- Small production bundle (purged)
- Works well with React
- Easy responsive design
Configuration:
// tailwind.config.js
module.exports = {
content: ['./src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {
colors: {
primary: '#3B82F6',
secondary: '#10B981',
},
},
},
plugins: [],
};Choice: shadcn/ui (copy-paste components)
Rationale:
- Not a dependency, just source code
- Built on Radix UI primitives
- Accessible by default
- Customizable
- Tailwind-based
Components to Use:
- Button, Input, Dialog
- Dropdown, Tabs
- Toast notifications
- Card layouts
Choice: Native WebRTC APIs (RTCPeerConnection)
Rationale:
- No additional dependencies
- Full control over configuration
- Works in Electron and browsers
- Standard, well-documented
Key APIs:
// Peer connection
const pc = new RTCPeerConnection({
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'turn:turn.pairux.com:3478', username: '...', credential: '...' },
],
});
// Media track (screen share)
const stream = await navigator.mediaDevices.getDisplayMedia({
video: { cursor: 'always' },
audio: false,
});
pc.addTrack(stream.getVideoTracks()[0], stream);
// Data channel (input events)
const dc = pc.createDataChannel('input', { ordered: true });Choice: Consider simple-peer for simplified API
Rationale:
- Abstracts WebRTC complexity
- Handles offer/answer exchange
- Simpler event handling
- Well-tested
Trade-off: Less control, but faster development
Choice: Supabase (managed)
Rationale:
- Integrated auth + database + realtime
- PostgreSQL underneath
- Row Level Security
- Realtime subscriptions for signaling
- Generous free tier
- Self-hostable if needed later
Services Used:
| Service | Purpose |
|---|---|
| Auth | User authentication |
| Database | Session/participant storage |
| Realtime | WebRTC signaling |
| Storage | (Future) Session recordings |
Choice: Supabase Realtime Channels
Rationale:
- No custom signaling server needed
- Built-in presence
- Scales automatically
- Integrated with auth
Channel Structure:
// Session channel
const channel = supabase.channel(`session:${sessionId}`);
// Broadcast signaling messages
channel.send({
type: 'broadcast',
event: 'signal',
payload: { type: 'offer', sdp: '...' },
});
// Listen for signals
channel.on('broadcast', { event: 'signal' }, (payload) => {
// Handle offer/answer/ice-candidate
});Choice: Self-hosted coturn
Rationale:
- Open source, battle-tested
- Full control over infrastructure
- No per-minute costs
- Already familiar (bittorrented.com)
Deployment:
- Docker container or bare metal
- TLS certificates required
- Firewall rules for UDP/TCP ports
Configuration (turnserver.conf):
listening-port=3478
tls-listening-port=5349
realm=pairux.com
server-name=turn.pairux.com
# Authentication
lt-cred-mech
user=pairux:password
# TLS
cert=/etc/ssl/turn.pem
pkey=/etc/ssl/turn.key
# Logging
log-file=/var/log/turnserver.logAlternatives Considered:
| Service | Rejected Because |
|---|---|
| Twilio TURN | Per-minute costs add up |
| Xirsys | Same cost concerns |
| Metered.ca | Less control |
Choice: TypeScript 5+
Rationale:
- Type safety across codebase
- Better IDE support
- Catches errors at compile time
- Self-documenting code
- Required for shared packages
Configuration (tsconfig.json base):
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true
}
}Choice: ESLint v8 + Prettier
Rationale:
- Industry standard
- Consistent code style
- Catches common errors
- Auto-fixable
Plugins:
@typescript-eslinteslint-plugin-reacteslint-plugin-react-hookseslint-config-prettier
Unit Tests: Vitest
- Fast, Vite-native
- Jest-compatible API
- TypeScript support
E2E Tests: Playwright
- Cross-browser testing
- Electron support
- Visual regression
Component Tests: React Testing Library
- User-centric testing
- Works with Vitest
Choice: GitHub Actions
Rationale:
- Integrated with GitHub
- Matrix builds for cross-platform
- Good caching support
- Free for public repos
Key Workflows:
- PR checks (lint, build, test)
- Web deploy (Vercel)
- Desktop release (electron-builder)
- Package manager updates
Choice: electron-builder
Rationale:
- All-in-one packaging solution
- Code signing support
- Auto-update support
- Multiple output formats
Output Formats:
| Platform | Formats |
|---|---|
| macOS | DMG, PKG |
| Windows | MSI, NSIS |
| Linux | DEB, RPM, AppImage |
Purpose: TypeScript type definitions
Contents:
// Session types
export interface Session { ... }
export interface Participant { ... }
// WebRTC types
export interface SignalMessage { ... }
export interface InputEvent { ... }
// Control types
export type ControlState = 'view-only' | 'requested' | 'granted' | 'revoked';Purpose: Shared React components
Contents:
- Button, Input components
- Status indicators
- Loading states
Note: May not be needed if web and desktop UIs diverge significantly.
Purpose: WebRTC utilities
Contents:
// Connection management
export class PeerConnectionManager { ... }
// Signaling helpers
export function createOffer(): Promise<RTCSessionDescriptionInit>;
export function handleAnswer(answer: RTCSessionDescriptionInit): void;
// Input event serialization
export function serializeInputEvent(event: InputEvent): string;
export function deserializeInputEvent(data: string): InputEvent;| Technology | Version | Notes |
|---|---|---|
| Node.js | 24+ | Runtime |
| pnpm | 9+ | Package manager |
| Turborepo | 2+ | Build system |
| TypeScript | 5+ | Language |
| Electron | 28+ | Desktop framework |
| React | 19+ | UI library (bundled with Next.js 16) |
| Next.js | 16.2 | Web framework |
| Tailwind CSS | 4+ | Styling |
| shadcn/ui | Latest | UI components |
| Supabase | Latest | Backend |
| nut.js | 4+ | Input injection |
| electron-builder | 24+ | Packaging |
| Vitest | 1+ | Testing |
| Playwright | 1+ | E2E testing |
graph TD
subgraph Monorepo
Root[Root package.json]
subgraph Apps
Web[apps/web]
Desktop[apps/desktop]
end
subgraph Packages
Types[packages/shared-types]
WebRTC[packages/webrtc-core]
UI[packages/shared-ui]
end
end
Root --> Web
Root --> Desktop
Root --> Types
Root --> WebRTC
Root --> UI
Web --> Types
Web --> WebRTC
Web --> UI
Desktop --> Types
Desktop --> WebRTC
subgraph External
Electron[Electron]
NextJS[Next.js]
Supabase[Supabase SDK]
NutJS[nut.js]
end
Desktop --> Electron
Desktop --> NutJS
Desktop --> Supabase
Web --> NextJS
Web --> Supabase