Architecture Decisions

ADR-0003: Shared Package Strategy

Decision to organize shared code into domain-focused packages with clear boundaries

Status

Accepted

Date: 2025-02-03

Context

In a monorepo with multiple apps (CalNexus, Lexilink, Qript), we need a clear strategy for sharing code. Without careful design, we risk:

  • Tight coupling: Changes in one app breaking another
  • Circular dependencies: Packages depending on each other in complex ways
  • Bloated packages: “Kitchen sink” packages that try to do everything
  • Unclear ownership: No one knows who maintains what
  • Inconsistent patterns: Each app reinventing the same logic

We needed to define:

  • What belongs in a shared package vs. app-specific code
  • How packages depend on each other
  • Naming conventions and structure
  • How to version and evolve packages

Decision

We organized shared code into domain-focused packages with clear boundaries:

Package Categories

  1. Core Infrastructure (stable, rarely changes)

    • @hn-monorepo/ui — shadcn/ui components + design system
    • @hn-monorepo/config-typescript — Shared TS config
    • @hn-monorepo/config-biome — Linting/formatting config
  2. Business Logic (evolves with products)

    • @hn-monorepo/auth — Authentication wrappers (Convex Auth)
    • @hn-monorepo/billing — Stripe integration and subscription logic
    • @hn-monorepo/storage — File upload/download helpers
    • @hn-monorepo/analytics — Event tracking abstractions
  3. Integrations (external APIs)

    • @hn-monorepo/email — Email templates and nodemailer
    • @hn-monorepo/ai — AI SDK wrappers (Anthropic, OpenAI)
    • @hn-monorepo/i18n — next-intl configuration
  4. Development (tooling)

    • @hn-monorepo/testing — Shared Vitest/Playwright setup
    • @hn-monorepo/monitoring — OTel configuration

Package Rules

  1. Packages depend on packages, never on apps
  2. Packages are small and focused — each has a single, clear purpose
  3. Packages export only what is needed — use index.ts to control public API
  4. Packages are tree-shakeable — export individual functions/components, avoid side effects
  5. Apps can do whatever they want — app-specific logic stays in the app

Example Package Structure

// packages/billing/
├── src/
│   ├── index.ts          // Public API
│   ├── stripe.ts         // Stripe client setup
│   ├── webhooks.ts       // Webhook handlers
│   └── subscriptions.ts  // Subscription logic
├── package.json
└── tsconfig.json
// apps/calnexus/app/api/webhooks/stripe/route.ts
import { handleStripeWebhook } from "@hn-monorepo/billing"

export async function POST(req: Request) {
  return handleStripeWebhook(req)
}

Consequences

Positive

  • Clear boundaries: Easy to know where code belongs
  • Reusability: Write once, use in all apps
  • Type safety: Packages enforce contracts between apps
  • Independent evolution: Can update packages without touching apps (usually)
  • Easier testing: Test packages in isolation
  • Reduced duplication: No more copy-paste between projects

Negative

  • Abstraction overhead: Sometimes packages add indirection
  • Breaking changes are painful: Changing a package API affects all apps
  • Over-engineering risk: May create packages prematurely
  • Build complexity: Changes to packages require rebuilding dependent apps

Neutral

  • Requires discipline: Team must follow the rules consistently
  • Versioning strategy: Currently using workspace:*, may need semantic versioning later

Alternatives Considered

Alternative 1: No Shared Packages (Duplication)

  • Description: Each app owns all its code, copy-paste when needed
  • Pros: Complete independence; no coordination overhead
  • Cons: Massive duplication; bugs fixed in one app but not others; inconsistent UX
  • Why not chosen: Defeats the purpose of a monorepo

Alternative 2: Single “Kitchen Sink” Package

  • Description: One @hn-monorepo/shared with everything
  • Pros: Simple dependency graph; one place to look for shared code
  • Cons: Bloated, hard to navigate; unclear ownership; bundle size issues
  • Why not chosen: Does not scale as codebase grows

Alternative 3: Feature-Based Packages

  • Description: Packages organized by feature (e.g., @hn-monorepo/calendar)
  • Pros: Aligned with product domains; could be extracted as standalone products
  • Cons: Unclear where cross-cutting concerns go; risk of tight coupling to specific apps
  • Why not chosen: Our products are too different; domain packages work better

References

Notes

  • We use workspace:* for internal package dependencies (managed by Bun)
  • Packages should remain framework-agnostic when possible
  • When in doubt, keep code in the app until you need it in a second place (YAGNI principle)
HanseNexus 2026