Skip to content

adi2355/cross-platform-shared-contracts

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Terminal header

Overview

@shared/contracts is the canonical contract layer for a distributed, offline-first health platform. It defines every API schema, entity type, sync rule, conflict resolution policy, and health metric configuration shared between the mobile frontend and cloud backend. The package produces identical runtime behavior in Node.js and React Native — same Zod validation, same merge outcomes, same SHA-256 hashes, same cursor semantics. One definition, two enforcement layers, zero semantic drift.


Technology Stack

Language TypeScript ES2020
Validation Zod Zod Refinements
Target Runtimes Node.js React Native
Cryptography SHA-256 Web Crypto API
Testing Jest ts-jest

Engineering Principles

1. Define data shapes once, enforce everywhere

Every API request and response is validated by the same Zod schema on both client and server. There is exactly one definition for each entity type, metric code, and sync cursor format. If the shape changes, it changes in one place.

2. Make conflict resolution auditable and deterministic

Conflict resolution rules are declarative configuration, not imperative code scattered across services. Each entity type has a published ENTITY_CONFLICT_CONFIG with field-level merge policies. Both frontend and backend import and apply the same rules.

3. Encode domain invariants in types, not comments

Monotonic state transitions (e.g., session status: ACTIVE → PAUSED → COMPLETED), allowed health metric units, precision policies — these are encoded as TypeScript types and Zod refinements, not documentation that can drift.

4. Keep shared code pure and dependency-free

Every export is a pure function, frozen configuration object, or Zod schema. No side effects, no I/O, no runtime dependencies beyond zod. The package works identically in Node.js and React Native.


Architecture

graph LR
    subgraph Mobile Frontend
        UI[UI Components] --> FServ[Application Services]
        FServ --> FDB[Local SQLite DB]
    end

    subgraph Cloud Backend
        Ctrl[API Controllers] --> BServ[Business Logic]
        BServ --> BDB[PostgreSQL]
    end

    subgraph Shared Contracts
        ZOD[API Schemas · Zod]
        ENT[Entity Definitions]
        SYNC[Sync Configuration]
        HC[Health Config]
        MOD[Domain Models]
    end

    FServ -->|validates requests| ZOD
    FServ -->|sync logic| SYNC
    FServ -->|metric config| HC
    FDB -->|schema alignment| ENT

    Ctrl -->|validates payloads| ZOD
    BServ -->|sync logic| SYNC
    BServ -->|metric config| HC
    BServ -->|domain types| MOD
    BDB -->|schema alignment| ENT

    style ZOD fill:#FFFFE0,stroke:#333
    style ENT fill:#FFFFE0,stroke:#333
    style SYNC fill:#FFFFE0,stroke:#333
    style HC fill:#FFFFE0,stroke:#333
    style MOD fill:#FFFFE0,stroke:#333
Loading

API & Domain Contracts

Request/response schemas and DTOs for every API surface, validated at both client and server with the same Zod definitions.

Contract Purpose
health.contract.ts Batch health sample upload/response schemas, metric validation, payload hashing, local DB schemas
health-projection.contract.ts Rollup, sleep summary, session impact, product impact DTOs with ProjectionFreshnessMeta
journal.contract.ts Journal entry and effect schemas with mood, tags, version-based conflict detection
session-telemetry.contract.ts Time-series metric data with 1m/5m resolution bucketing
prediction.contract.ts Inventory depletion forecasts with confidence tiers and data quality classification
sync-lease.contract.ts Admission control schemas for rate-limited sync operations
sync-etag.contract.ts FNV-1a weak ETag generation for sync state validation
onboarding.contract.ts Onboarding preference storage with question versioning

Sync Engine Configuration

Declarative configuration driving bidirectional offline-first sync between SQLite (mobile) and PostgreSQL (backend). Entity ordering, cursor management, conflict resolution, and merge logic are all configuration — not imperative code.

Module Purpose
entity-types.ts 9 canonical entity types with sync order hierarchy (10–90) and legacy name mapping
conflict-configs.ts Per-entity conflict resolution: field-level policies for sessions, consumptions, journals, purchases, devices
conflict-strategies.ts 6 entity strategies (SERVER_WINS, MERGE, ...) and 11 field policies (LOCAL_WINS, MONOTONIC, MERGE_ARRAYS, MAX_VALUE, ...)
cursor.ts Composite cursor encode/decode with schema versioning and monotonic advancement validation
entity-merger.ts Pure merge functions driven by ENTITY_CONFLICT_CONFIG — deterministic on any runtime
conflict-resolution.ts Conflict outcome types: ADOPT, REBASE, MANUAL
product-field-mask.ts 23-field allowlist for product sync shaping
session-window.ts Canonical session semantics: idle timeout, hit accumulation, status transitions
relation-graph.ts Foreign key relationships and cascade rules for sync dependency ordering

Health Data Configuration

Canonical metric definitions, validation rules, cryptographic hashing, and projection state management for a 32-metric health data pipeline.

Module Purpose
metric-types.ts 32 HealthMetricCode definitions across 8 categories — units, bounds, value kinds, aggregation rules
payload-hash.ts Cross-platform SHA-256 hashing with order-invariant canonicalization for request idempotency
precision-policy.ts Per-metric decimal precision rules and value rounding enforcement
freshness-types.ts Projection state machine: READY, COMPUTING, STALE, FAILED, NO_DATA with factory functions
sleep-clustering.ts Gap-based sleep session clustering with nap vs. night detection
sleep-utils.ts Night boundary anchoring (18:00–12:00 local), timezone-aware date computation
source-resolution.ts Multi-device source deduplication and priority ranking
unit-normalization.ts Cross-platform unit aliasing and conversion

Hardest Problems Solved

1. Cross-platform order-invariant SHA-256 payload hashing

Health data uploads must be idempotent across retries on unreliable mobile networks. computeBatchPayloadHash canonicalizes samples and deletions into a deterministic byte sequence — sorting by composite key, normalizing undefined/null values, applying recursive key ordering — then produces an identical SHA-256 hash on both Node.js (crypto) and React Native (expo-crypto) via globalThis.crypto.subtle. The server detects retried requests by hash match, returning the cached response instead of reprocessing.

2. Deterministic conflict resolution across independently operating clients

When a mobile client and backend server both modify the same entity offline, they must independently reach identical merge outcomes. ENTITY_CONFLICT_CONFIG defines per-entity, per-field merge policies (LOCAL_WINS, SERVER_WINS, MONOTONIC, MERGE_ARRAYS, MAX_VALUE). The EntityMerger applies these policies as pure functions — no I/O, no service dependencies. Both sides import the same configuration and execute the same logic. The outcome is reproducible from the config alone.

3. Monotonic cursor advancement with backward-movement detection

Cursor-based sync pagination must never regress, or data is silently lost. The composite cursor system encodes per-entity cursor state (timestamps + UUIDs) with a schema version field, validates monotonic advancement on every decode, and rejects backward cursor movement with typed errors (CursorBackwardError). Schema versioning enables cursor format evolution without breaking existing clients.


Contract Domains

Domain Scope Key Exports
API Contracts Request/response schemas for all REST endpoints BatchUpsertSamplesRequestSchema, JournalEntrySchema, SyncLeaseRequestSchema
Health Configuration 32 metric definitions, units, bounds, and payload hashing HEALTH_METRIC_CONFIG, computeBatchPayloadHash, PrecisionPolicy
Sync Configuration Entity ordering, cursor management, and conflict resolution ENTITY_CONFLICT_CONFIG, encodeCompositeCursor, EntityMerger
Health Projections Read-model DTOs for aggregated health data toHealthRollupDayDto, toSleepNightSummaryDto, FreshnessMeta
Domain Models Shared entity type definitions and enums JournalEntry, JournalEffect, EffectPolarity

Design Patterns & Guarantees

Pattern Implementation
Runtime Schema Validation Zod .parse() / .safeParse() at every trust boundary with actionable error messages
Frozen Configuration Object.freeze() on all shared config — prevents mutation across Node.js module cache
Pure Functions All merge, hash, and cursor utilities are stateless and side-effect free
Cross-Platform Crypto globalThis.crypto.subtle abstraction — identical hashes on Node.js and React Native
Order-Invariant Hashing Samples sorted by composite key before SHA-256 — field order never affects the hash
Monotonic State Machines MONOTONIC field policy enforces forward-only state transitions (e.g., ACTIVE → COMPLETED)
Cursor Schema Versioning Version field in encoded cursors enables format evolution without breaking clients
Metadata Size Guards Health sample metadata enforces 20 key max, 3-level depth, 4KB ceiling
Backward Compatibility Legacy model name mappings and configVersion field for graceful schema evolution
Array Deduplication MERGE_ARRAYS policy unions arrays with automatic deduplication
Type-Safe Error Codes Exhaustive error code enums with retryable/non-retryable classification
Canonical Entity Ordering Sync order (10–90) encoded in configuration, respecting foreign key dependencies

Usage

// Backend: Validate incoming API request
import { BatchUpsertSamplesRequestSchema } from '@shared/contracts';
const validated = BatchUpsertSamplesRequestSchema.parse(req.body);

// Mobile: Conflict resolution during sync
import { ENTITY_CONFLICT_CONFIG, CONFLICT_STRATEGY } from '@shared/contracts';
const sessionConfig = ENTITY_CONFLICT_CONFIG.sessions;
// sessionConfig.defaultStrategy === CONFLICT_STRATEGY.MERGE

// Both: Cursor encoding for sync pagination
import { encodeCompositeCursor, decodeCompositeCursor } from '@shared/contracts';
const cursor = encodeCompositeCursor(entityCursors);

// Both: Payload hash for idempotent health uploads
import { computeBatchPayloadHash } from '@shared/contracts';
const hash = await computeBatchPayloadHash({ samples, deleted, configVersion });

// Both: Health metric validation
import { isHealthMetricCode, HEALTH_METRIC_CONFIG } from '@shared/contracts';
if (isHealthMetricCode('heart_rate')) {
  const config = HEALTH_METRIC_CONFIG['heart_rate'];
  // config.canonicalUnit === 'bpm', config.valueKind === 'SCALAR_NUM'
}

// Both: Projection freshness state
import { createStaleFreshnessMeta, getFreshnessUiState } from '@shared/contracts';
const meta = createStaleFreshnessMeta(existingMeta, 'NEW_SAMPLES');
const ui = getFreshnessUiState(meta.status);
// ui.showStaleBadge === true, ui.showData === true

Folder Structure

src/
├── contracts/                      # API request/response schemas and DTOs
│   ├── health.contract.ts              # Health sample schemas, batch upsert, payload hashing
│   ├── health-projection.contract.ts   # Projection DTOs, freshness meta, response summaries
│   ├── journal.contract.ts             # Journal entry/effect schemas
│   ├── session-telemetry.contract.ts   # Time-series telemetry data
│   ├── prediction.contract.ts          # Inventory prediction schemas
│   ├── sync-etag.contract.ts           # ETag generation for sync
│   ├── sync-lease.contract.ts          # Sync lease admission control
│   ├── onboarding.contract.ts          # Onboarding preferences
│   └── onboarding-questions.ts         # Question definitions
├── health-config/                  # Health data configuration & utilities
│   ├── metric-types.ts                 # 32 HealthMetricCode definitions with units and bounds
│   ├── payload-hash.ts                 # Cross-platform SHA-256 payload hashing
│   ├── precision-policy.ts             # Per-metric decimal precision
│   ├── freshness-types.ts              # Projection state machine and factory functions
│   ├── sleep-clustering.ts             # Sleep session clustering logic
│   ├── sleep-utils.ts                  # Night boundary and anchor date utilities
│   ├── source-resolution.ts            # Multi-device source deduplication
│   └── unit-normalization.ts           # Unit aliasing and conversion
├── models/                         # Shared data model types
│   └── journal.model.ts               # Journal entry and effect models
├── sync-config/                    # Sync engine configuration & logic
│   ├── entity-types.ts                # 9 canonical entity types and sync order
│   ├── conflict-configs.ts            # Per-entity conflict resolution rules
│   ├── conflict-strategies.ts         # Strategy and field policy enums
│   ├── conflict-resolution.ts         # Conflict outcome types
│   ├── cursor.ts                      # Composite cursor encode/decode/advance
│   ├── entity-merger.ts               # Pure config-driven merge functions
│   ├── product-field-mask.ts          # Product field allowlist
│   ├── relation-graph.ts              # Entity dependency graph
│   └── session-window.ts              # Session window semantics
└── index.ts                        # Central re-export hub

Design Decisions

Why Zod instead of just TypeScript types?

TypeScript types are erased at runtime. API payloads arrive as unknown — they need runtime validation. Zod provides both: z.infer<typeof Schema> gives the static type, and Schema.parse(data) validates at the trust boundary. One definition, two enforcement layers.

Why shared conflict configuration instead of per-service logic?

When the mobile client and backend server both resolve conflicts, they must agree on the outcome. Shared configuration (ENTITY_CONFLICT_CONFIG) ensures deterministic, reproducible results. Tests can verify conflict resolution without running either service.

Why freeze all configuration objects?

Object.freeze prevents accidental mutation of shared state. A service importing ENTITY_CONFLICT_CONFIG cannot accidentally modify it for all other consumers. This is especially important in Node.js where module caching means all importers share the same object reference.

Why cross-platform payload hashing?

computeBatchPayloadHash produces identical SHA-256 hashes on both client and server for the same payload. This enables request-level idempotency: the server detects retried requests by matching the hash, returning the cached response instead of reprocessing.


Platform Context

This package is one of four repositories that compose the full platform:

Repository Role
ESP32-S3 Edge Firmware Platform BLE sensor firmware on ESP32-S3 with FreeRTOS
Offline-First Mobile Platform React Native app with native iOS/Android runtime integrations
Cloud-Native Backend Platform Event-driven Node.js backend with PostgreSQL and BullMQ
Cross-Platform Shared Contracts (this repo) Canonical TypeScript contracts shared across all platform layers

Terminal footer

About

Cross-platform TypeScript contract layer: Zod API schemas, deterministic conflict resolution, cursor-based sync configuration, SHA-256 payload hashing, and health metric definitions for offline-first distributed systems

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors