Design Decisions
Key architectural decisions made in Wellpipe and their rationale.
ADR-001: Separate Package Per Provider
Decision: Each health provider (WHOOP, Oura, Fitbit) is a separate npm package.
Rationale:
- Users only install providers they need
- Smaller bundle sizes
- Independent versioning and releases
- Isolated dependencies
- Easier testing and maintenance
Alternatives Considered:
- Monolithic package with all providers → Too large, unnecessary dependencies
- Plugin architecture → More complexity for minimal benefit
ADR-002: Core Package Has Minimal Dependencies
Decision: @wellpipe/core only depends on zod.
Rationale:
- Reduces supply chain risk
- Smaller install size
- Zod provides runtime validation essential for API responses
- Forces clean interface design
Consequences:
- Some utilities that could use libraries are implemented manually
- More boilerplate in some cases
ADR-003: Providers Don't Handle OAuth
Decision: Provider packages (like @wellpipe/whoop) don't implement OAuth flows. They receive tokens from the consuming application.
Rationale:
- OAuth flow varies by deployment (CLI vs Cloud)
- Token storage varies (file vs database)
- Providers focus on API integration only
- Cleaner separation of concerns
Implementation:
// Provider expects a TokenProvider
const client = new WhoopClient({ tokenProvider });
// CLI implements TokenProvider with .env storage
class EnvTokenProvider implements TokenProvider { ... }
// Cloud implements TokenProvider with encrypted database
class DatabaseTokenProvider implements TokenProvider { ... }
ADR-004: MCP Tools as Data, Not Functions
Decision: MCP tools are defined as data structures that can be introspected, not just as handler functions.
Rationale:
- Enables tool documentation generation
- Allows schema validation
- Tools can be filtered/composed
- Easier testing
Implementation:
const tool = {
name: 'get-recent-sleep',
description: 'Get sleep data from the last N days',
inputSchema: z.object({
days: z.number().default(7)
}),
handler: async (args, client) => { ... }
};
ADR-005: TypeScript Strict Mode Everywhere
Decision: All packages use TypeScript strict mode with explicit return types on public functions.
Rationale:
- Catches more bugs at compile time
- Improves documentation through types
- Better IDE experience
- Forces intentional API design
Configuration:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitReturns": true
}
}
ADR-006: Zod for Runtime Validation
Decision: Use Zod for all runtime validation of external data (API responses, user input).
Rationale:
- TypeScript types don't exist at runtime
- API responses must be validated
- Zod provides excellent error messages
- Can infer TypeScript types from schemas
Implementation:
const SleepSchema = z.object({
id: z.string(),
start: z.string().datetime(),
score: z.object({
sleep_performance_percentage: z.number()
})
});
type Sleep = z.infer<typeof SleepSchema>;
ADR-007: Token Encryption with AES-256-GCM
Decision: OAuth tokens in the cloud are encrypted with AES-256-GCM.
Rationale:
- AES-256 is industry standard
- GCM mode provides authentication (prevents tampering)
- Unique IV per encryption prevents pattern analysis
- Protects tokens even if database is compromised
Trade-offs:
- Slight performance overhead for encryption/decryption
- Key management complexity
ADR-008: No Health Data Storage
Decision: Wellpipe never stores health data. It's fetched on-demand and passed through.
Rationale:
- Minimizes data liability
- Simplifies privacy compliance
- Users retain control over their data
- No data sync issues
Consequences:
- Every request hits the provider API
- Subject to provider rate limits
- No offline access
ADR-009: Multi-Repo Structure
Decision: Wellpipe uses multiple repositories (core, provider-whoop, cli, cloud, docs) rather than a monorepo.
Rationale:
- Clear separation between public and private code
- Independent release cycles
- Simpler CI/CD per repository
- Easier open-source contributions
Alternatives Considered:
- Monorepo with Turborepo/Nx → More complex tooling, harder to separate public/private
- Single repo → Would require complex access controls
ADR-010: Vitest for Testing
Decision: Use Vitest for all testing across packages.
Rationale:
- Native ESM support
- Fast execution
- Jest-compatible API
- Excellent TypeScript support
- Built-in coverage
Convention:
- Test files colocated with source (
*.test.ts) - Mock external APIs, never hit real endpoints
- Unit tests for logic, integration tests for flows
ADR-011: Next.js for Cloud Backend
Decision: Use Next.js for the cloud SaaS application.
Rationale:
- API routes and UI in one framework
- Excellent TypeScript support
- Easy deployment to Vercel
- Strong community and ecosystem
Trade-offs:
- Some overhead compared to pure API server
- Opinionated structure
ADR-012: OAuth State in Crypto-Random Tokens
Decision: Use cryptographically random state parameters for OAuth CSRF protection.
Rationale:
- Prevents CSRF attacks on OAuth flows
- Standard security practice
- State can encode additional context if needed
Implementation:
const state = randomBytes(32).toString('hex');
// Store state in session or signed cookie
// Verify on callback