Skip to main content

Architecture Overview

Cascadia PLM is a code-first Product Lifecycle Management system. This document provides a mental model of the system architecture and explains key design decisions.

Detailed guides: For implementation specifics, see Service Patterns, Git-Style Versioning, and Database Patterns.

System Mental Model

┌─────────────────────────────────────────────────────────────────────────────┐
│ Cascadia PLM │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Parts │ │ Documents │ │ Change │ │Requirements │ │
│ │ │ │ │ │ Orders │ │ & Tasks │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │ │
│ └──────────────────┴────────┬─────────┴──────────────────┘ │
│ │ │
│ ┌─────────▼─────────┐ │
│ │ Item Registry │ ← Unified item system │
│ │ & Services │ │
│ └─────────┬─────────┘ │
│ │ │
│ ┌───────────────────────────┼───────────────────────────┐ │
│ │ │ │ │
│ ┌──────▼──────┐ ┌────────▼────────┐ ┌────────▼────────┐ │
│ │ Versioning │ │ Workflows │ │ File Vault │ │
│ │ (Git-style)│ │ & Lifecycles │ │ │ │
│ └─────────────┘ └─────────────────┘ └─────────────────┘ │
│ │
├─────────────────────────────────────────────────────────────────────────────┤
│ PostgreSQL Database │
└─────────────────────────────────────────────────────────────────────────────┘

Technology Stack

LayerTechnologyWhy This Choice
FrameworkTanStack StartFull-stack TypeScript, file-based routing, excellent DX
DatabasePostgreSQL 18+ACID compliance, JSON support, proven at scale
ORMDrizzleType-safe, SQL-like syntax, great migrations
UITailwind + RadixUtility-first CSS, accessible components
ValidationZodRuntime validation with TypeScript inference
AuthOslo.js + ArcticModern crypto, OAuth support, no magic
TestingVitest + PlaywrightFast unit tests, reliable E2E

Core Design Decisions

1. Code-First Configuration

Decision: All customization happens in TypeScript code, not UI configuration.

Why:

  • Version controlled with Git
  • Full IDE support (autocomplete, refactoring, type checking)
  • No hidden configuration in database
  • Deployments are reproducible
  • Easier to review changes in PRs

Trade-off: Less accessible to non-developers, but PLM customization typically requires developer skills anyway.

2. Two-Table Pattern for Items

Decision: Every item type has a base record in items plus type-specific fields in a dedicated table.

items (base)              parts (type-specific)
├── id ├── itemId (FK) ─────┐
├── masterId ├── makeBuy │
├── itemNumber ├── material │
├── revision └── weight │
├── state │
└── ... ◄─────────────────────┘

Why:

  • Unified queries across all item types ("show me all items in Review state")
  • Common operations work on any item (revise, transition, search)
  • Type-specific tables stay focused and small
  • Relationships point to items.id, not type-specific IDs

Learn more: Database Patterns

3. Git-Style Versioning

Decision: Use branches and commits instead of linear revision sequences.

Traditional PLM:     PN-001: A → B → C → D (linear, blocking)

Cascadia: main: [A] ────────── [B] ────── [C]
│ ↑ ↑
eco/ECO-1: └──[work]─────┘ │
eco/ECO-2: ──────────[work]─────────┘

Why:

  • Multiple ECOs can work on the same part simultaneously
  • ECO cancellation is clean (just delete the branch)
  • Changes are grouped by purpose (ECO), not just "next revision"
  • Complete history with merge commits
  • Revision letters assigned only on merge (release)

Trade-off: More complex than linear revisions, but solves real workflow problems.

Learn more: Git-Style Versioning

4. Service Layer Pattern

Decision: All business logic in service classes, not in routes or components.

Route (thin)          Service (business logic)       Database
│ │ │
│ validate request │ │
│ ──────────────────► │ │
│ │ complex logic │
│ │ ──────────────────────► │
│ │ ◄────────────────────── │
│ ◄────────────────── │ │
│ return response │ │

Why:

  • Routes stay thin and focused on HTTP concerns
  • Business logic is testable without HTTP
  • Services can call other services
  • Easier to understand "where does X happen?"

Learn more: Service Patterns

5. Program-Based Permissions

Decision: Users belong to Programs, which control what they can access.

Organization (tenant)
└── Program A (permission boundary)
│ ├── Design 1
│ │ └── Parts, Documents, etc.
│ └── Design 2
└── Program B (separate boundary)
└── Design 3

Why:

  • Natural fit for contract-based work (defense, aerospace)
  • Clear data isolation between programs
  • Users can belong to multiple programs
  • Simpler than item-level ACLs for most use cases

6. Workflow Definitions as Data

Decision: Workflows and lifecycles are defined as JSON structures in the database, not hardcoded.

Why:

  • Different item types can share lifecycle definitions
  • Admins can modify workflows without code deployment
  • Workflow history is tied to the definition version
  • Supports approval voting, guards, and effects

Trade-off: More complex than hardcoded states, but necessary for enterprise flexibility.

Directory Structure

src/
├── lib/
│ ├── items/ # Item type system
│ │ ├── registry.ts # Central registry
│ │ ├── types/ # Type definitions and schemas
│ │ └── services/ # ItemService, ChangeOrderService
│ │
│ ├── services/ # Versioning services
│ │ ├── DesignService.ts
│ │ ├── BranchService.ts
│ │ ├── CommitService.ts
│ │ ├── CheckoutService.ts
│ │ └── VersionResolver.ts
│ │
│ ├── db/
│ │ ├── index.ts # Database connection
│ │ └── schema/ # Drizzle table definitions
│ │
│ ├── auth/ # Authentication
│ │ ├── session.ts # Session management
│ │ ├── password.ts # Password hashing
│ │ └── server.ts # Route helpers
│ │
│ └── vault/ # File storage
│ └── services/

├── routes/
│ ├── api/ # API endpoints
│ └── (app)/ # UI routes

└── components/
├── ui/ # Base components
└── items/ # Item-specific components

Data Flow

Read Path (e.g., viewing a part)

1. Browser requests /parts/[id]
2. UI route loads, calls API
3. API route: requireAuth() → requirePermission()
4. ItemService.findById() called
5. Drizzle queries items + parts tables
6. Merged result returned to UI
7. React renders PartDetail component

Write Path (e.g., ECO release)

1. User clicks "Release ECO" button
2. API route receives POST request
3. requirePermission('ChangeOrder', 'update')
4. EcoReleaseService.releaseEco() called
5. Transaction begins:
a. Validate ECO is approved
b. For each affected design:
- Merge ECO branch to main
- Assign revision letters
- Create merge commit
- Archive ECO branch
c. Update ECO state to Implemented
6. Transaction commits
7. Success response returned

Integration Points

External Systems

IntegrationMethodStatus
SolidWorksDesktop add-in (C#)In progress
SysML 2.0REST API at /api/sysml/*Complete
ERP SystemsPlanned webhook eventsFuture
SSO/SAMLArctic OAuth providersSupported

Extension Patterns

Adding a new item type: Define schema, create type file, register in registry. See Adding Item Types.

Custom workflow actions: Add action handlers to workflow definition.

External triggers: API endpoints accept webhooks for external events.

Key Architectural Boundaries

Client vs Server

Client (Browser)                    Server (Node.js)
───────────────── ─────────────────
React components API routes
TanStack Router Services
Form state Database access
Local validation Authoritative validation

│ HTTP/JSON

Rule: Database code never runs in the browser. Use .server.ts suffix or dynamic imports for server-only code.

Item Types vs Services

Item Types (data shape)             Services (behavior)
─────────────────────── ───────────────────
Zod schemas Business logic
Type-specific fields State transitions
UI components Cross-type operations
Registry config Database transactions

Rule: Services are type-agnostic where possible. ItemService.create('Part', data) works for any registered type.

Performance Considerations

Database

  • Indexes: Key queries have covering indexes (see Database Patterns)
  • Pagination: All list endpoints support limit/offset
  • Soft deletes: Deleted items excluded from queries by default

Caching

  • Session cache: Sessions cached in memory with DB fallback
  • Registry cache: Item type configs cached after first load
  • No query cache: Drizzle queries hit DB directly (PostgreSQL handles caching)

Bundle Size

  • Code splitting: Routes are lazy-loaded
  • Server components: Heavy logic stays server-side
  • Tree shaking: Unused code eliminated in production build

Further Reading

TopicDocument
Using servicesService Patterns
Branching and commitsGit-Style Versioning
Schema and queriesDatabase Patterns
Coding standardsCode Conventions
Extending the systemAdding Item Types
Test infrastructureTesting