Skip to main content

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