Skip to main content

Overview

The @hyperscape/shared package is the core Hyperscape engine, providing:
  • Entity Component System (ECS)
  • Three.js WebGPU rendering (WebGPU required - no WebGL fallback)
  • PhysX physics simulation
  • Real-time networking
  • React UI components
  • Game data manifests
WebGPU Required: All rendering uses TSL (Three Shading Language) which only works with WebGPU. There is no WebGL fallback. Requires Chrome 113+, Edge 113+, or Safari 18+ (macOS 15+).

Package Location

packages/shared/
├── src/
│   ├── components/     # ECS components
│   ├── constants/      # Game constants
│   ├── core/           # Core engine classes
│   ├── data/           # Game manifests (npcs, items, etc.)
│   ├── entities/       # Entity definitions
│   │   ├── player/     # PlayerEntity, PlayerLocal, PlayerRemote
│   │   ├── npc/        # MobEntity, NPCEntity
│   │   ├── world/      # World entities
│   │   └── managers/   # Entity managers
│   ├── extras/         # Additional utilities
│   ├── libs/           # External integrations
│   ├── nodes/          # Scene graph nodes
│   ├── physics/        # PhysX wrapper
│   ├── platform/       # Platform detection
│   ├── runtime/        # Game loop
│   ├── systems/        # ECS systems
│   │   ├── client/     # Client-only systems
│   │   ├── server/     # Server-only systems
│   │   └── shared/     # Shared systems (combat, economy, etc.)
│   ├── types/          # TypeScript types
│   └── utils/          # Helper functions
├── index.ts            # Server exports
└── index.client.ts     # Client exports

Terrain Generation System

The shared package includes a sophisticated procedural terrain generation system with multi-threaded performance and cached height lookups for optimal performance.

Terrain Height Cache System

The terrain system uses a tile-based cache for fast height queries without re-evaluating noise functions. Critical bugs in the cache indexing were fixed in commit 21e0860 (Feb 25, 2026). Cache Structure:
// From TerrainSystem.ts
private heightCache = new Map<string, Float32Array>();  // tileKey → height grid
private colorCache = new Map<string, Uint8Array>();     // tileKey → color grid

// Tile key format: "tileKey_x_z" (e.g., "tileKey_0_0", "tileKey_-1_2")
Critical Bug Fixes (commit 21e0860):
  1. Tile Index Calculation Error:
    • Issue: getHeightAtCached used Math.floor(worldX/TILE_SIZE) which doesn’t account for PlaneGeometry’s centered coordinates
    • Impact: Terrain tiles were indexed incorrectly, causing ~50m vertical offset in height queries
    • Fix: Added worldToTerrainTileIndex() canonical helper
    • Formula:
      export function worldToTerrainTileIndex(worldCoord: number): number {
        return Math.floor((worldCoord + HALF_TILE_SIZE) / TILE_SIZE);
      }
      
    • Result: Terrain height queries now return correct values for all world positions
  2. Grid Index Formula Error:
    • Issue: Grid index formula omitted the halfSize offset from PlaneGeometry’s [-50,+50] range
    • Impact: Local coordinates within tiles were mapped incorrectly to height data arrays
    • Fix: Added localToGridIndex() canonical helper
    • Formula:
      export function localToGridIndex(localCoord: number, halfSize: number): number {
        return Math.floor(localCoord + halfSize);
      }
      
    • Result: Height data lookups within tiles now use correct array indices
  3. Color Cache Key Typo:
    • Issue: getTerrainColorAt() used comma separator (tileKey_x,z) instead of underscore (tileKey_x_z)
    • Impact: Color cache lookups always failed, forcing expensive recomputation
    • Fix: Corrected key format to match height cache convention
    • Result: Color cache now works correctly, improving terrain rendering performance
Usage Example:
// Get height at world position (uses cache if available)
const height = terrainSystem.getHeightAt(worldX, worldZ);

// Get terrain color at world position (uses cache if available)
const color = terrainSystem.getTerrainColorAt(worldX, worldZ);

// Both methods now use canonical helpers internally:
const tileX = worldToTerrainTileIndex(worldX);
const tileZ = worldToTerrainTileIndex(worldZ);
const tileKey = `tileKey_${tileX}_${tileZ}`;
const heightGrid = this.heightCache.get(tileKey);
if (heightGrid) {
  const localX = worldX - tileX * TILE_SIZE;
  const localZ = worldZ - tileZ * TILE_SIZE;
  const gridX = localToGridIndex(localX, HALF_TILE_SIZE);
  const gridZ = localToGridIndex(localZ, HALF_TILE_SIZE);
  const index = gridZ * resolution + gridX;
  return heightGrid[index];
}
Impact of Fixes:
  • Eliminates 50m vertical offset in player spawning and entity placement
  • Fixes vegetation placement using incorrect terrain heights
  • Prevents height query inconsistencies between cached and real-time lookups
  • Improves terrain color rendering performance via working color cache

TerrainHeightParams.ts

Single source of truth for all terrain generation parameters. Both TerrainSystem (main thread) and TerrainWorker (web worker) consume these values to ensure identical terrain generation:
// Noise layer definitions
export const CONTINENT_LAYER: NoiseLayerDef = {
  scale: 0.0008,
  octaves: 5,
  persistence: 0.7,
  lacunarity: 2.0,
  weight: 0.35,
};

export const HILL_LAYER: NoiseLayerDef = {
  scale: 0.02,
  octaves: 4,
  persistence: 0.6,
  lacunarity: 2.2,
  weight: 0.25,
};

// Island configuration
export const ISLAND_RADIUS = 350;
export const ISLAND_FALLOFF = 100;
export const BASE_ELEVATION = 0.42;

// Worker code generation
export function buildGetBaseHeightAtJS(): string {
  // Injects constants into worker string
}
Key Features:
  • Prevents parameter drift between main thread and workers
  • TypeScript constants injected into worker code at runtime
  • Changing a constant automatically updates both threads
  • Includes noise layers, island mask, pond, coastline, and mountain boost

Web Worker Terrain Generation

Terrain generation offloaded to web workers for parallel processing:
  • Worker Height Computation: Full height calculation including shoreline adjustment
  • Worker Normal Computation: Normals computed via (resolution+2)² overflow grid
  • Performance: 63x reduction in main-thread noise evaluations
  • Conditional Fallback: Main-thread recomputation only for tiles with flat zones (buildings/stations)
  • Parallel Processing: Utilizes multiple CPU cores while keeping main thread free

GPU-Instanced Rendering Systems

GLBTreeInstancer (commit 0871acb)

InstancedMesh-based tree rendering system that replaces per-tree scene.clone(true) with shared InstancedMesh pools per LOD level. Performance Impact:
  • Eliminated per-tree geometry cloning (saves ~2-5ms per tree spawn)
  • Reduced draw calls from N trees to 3 (one per LOD level)
  • FPS improvement: ~15-20% in dense forest areas
  • Memory savings: ~80% reduction in geometry buffer allocations
Implementation:
// From packages/shared/src/systems/shared/world/GLBTreeInstancer.ts
export class GLBTreeInstancer {
  private pools: Map<string, LODPool> = new Map();
  
  allocate(modelId: string, position: Vector3, rotation: number): InstanceHandle | null {
    // Allocates instance slot from pre-sized pool (max 1000 per LOD)
  }
  
  free(handle: InstanceHandle): void {
    // Returns instance slot to pool for reuse
  }
  
  setDepleted(handle: InstanceHandle, isDepleted: boolean): void {
    // Handles stump state transitions
  }
}
Integration:
  • Initialized in createClientWorld.ts when stage scene is ready
  • ResourceEntity routes GLB trees through instancer instead of scene.clone()
  • Handles depleted/stump/destroy lifecycle via no-op-safe calls

ResourceEntity Visual Strategy Pattern (commit bc60264)

Refactored ResourceEntity (~1700 lines removed) into delegated visual strategies using the Strategy Pattern. Visual Strategies:
  1. TreeGLBVisualStrategy: GLB tree models with LOD support
    • Uses GLBTreeInstancer for instanced rendering
    • Handles LOD transitions based on camera distance
    • Manages stump state transitions
  2. TreeProcgenVisualStrategy: Procedurally generated trees
    • Uses ProcgenTreeInstancer for procedural geometry
    • Supports runtime tree generation with L-system parameters
    • Handles leaf/branch color variations
  3. StandardModelVisualStrategy: Generic 3D models (rocks, ores)
    • Loads GLB models via ModelCache
    • Handles scale and rotation from manifest
    • Supports depleted model swapping
  4. FishingSpotVisualStrategy: Fishing spot particles
    • Registers with ParticleManager for GPU-instanced water effects
    • Handles ripple/splash/bubble animations
    • Manages particle lifecycle on depletion
  5. PlaceholderVisualStrategy: Fallback for missing models
    • Uses PlaceholderInstancer for instanced colored cubes
    • Color-coded by resource type (green=tree, brown=ore, blue=fishing)
    • Automatically used when modelPath is null or “null” string
Factory Pattern:
// From packages/shared/src/entities/world/visuals/createVisualStrategy.ts
export function createVisualStrategy(
  config: ResourceConfig,
  entity: ResourceEntity
): ResourceVisualStrategy {
  // Selects strategy based on resource type, model path, and manifest config
}
Benefits:
  • Single Responsibility: Each strategy handles one rendering approach
  • Open/Closed: Add new strategies without modifying ResourceEntity
  • Testability: Strategies can be tested in isolation
  • Maintainability: ~1700 lines of conditional logic replaced with clean delegation
PlaceholderInstancer:
  • Manages InstancedMesh pools for placeholder resources (trees/ores with missing models)
  • Color-coded: green (trees), brown (ores), blue (fishing spots)
  • Max 1000 instances per resource type
  • Initialized in createClientWorld.ts alongside GLBTreeInstancer
Bug Fixes:
  • Fixed fishing spot particles persisting after depletion (guard re-registration when depleted, zero ripple phase offset on unregister)
  • Fixed placeholder trees not rendering due to “null” string in modelPath (sanitize in ResourceSystem + createVisualStrategy factory)

Particle System

The shared package includes a centralized GPU-instanced particle system for high-performance visual effects. This system was introduced in PR #877 and provides a 97% reduction in draw calls with an 80% FPS improvement.

ParticleManager Architecture

Unified particle routing system that dispatches particle events to specialized sub-managers:
// From packages/shared/src/entities/managers/particleManager/
export { ParticleManager } from './ParticleManager';
export { WaterParticleManager } from './WaterParticleManager';
Core Components:
  • ParticleManager: Central router with ownership tracking and event dispatching
  • WaterParticleManager: GPU-instanced fishing spot effects (splash, bubble, shimmer, ripple)
Performance Benefits:
  • Reduces ~150 draw calls to 4 per fishing spot (97% reduction)
  • FPS improvement: 65-70 → 120 on reference hardware (80% increase)
  • ~450 lines of CPU animation code eliminated
  • Zero CPU overhead for particle updates (all on GPU via TSL shaders)
  • GPU-driven particle animations (parabolic arcs, wobble, twinkle, ring expansion)

WaterParticleManager Implementation

GPU-instanced water particle effects for fishing spots using InstancedMesh and TSL shaders: Technical Details:
  • 4 InstancedMeshes: splash, bubble, shimmer, ripple layers
  • TSL NodeMaterials: GPU-computed animations using Three.js Shading Language
  • InstancedBufferAttributes: Per-particle data storage
    • spotPos (vec3): fishing spot world center
    • ageLifetime (vec2): current age (x), total lifetime (y)
    • angleRadius (vec2): polar angle (x), radial distance (y)
    • dynamics (vec4): peakHeight (x), size (y), speed (z), direction (w)
  • Vertex Buffer Budget: 7 of 8 max attributes per particle layer
    • position(1) + uv(1) + instanceMatrix(1) + spotPos(1) + ageLifetime(1) + angleRadius(1) + dynamics(1)
  • Ripple Layer: 5 of 8 max attributes
    • position(1) + uv(1) + instanceMatrix(1) + spotPos(1) + rippleParams(1)
  • Pool Sizes: MAX_SPLASH=96, MAX_BUBBLE=72, MAX_SHIMMER=72, MAX_RIPPLE=24
Particle Animations:
  • Splash: Parabolic arc trajectories with pop-in fade
    • arcY = peakHeight * 4 * t * (1-t) for parabolic motion
    • Snappy fade-in (12x rate), smooth fade-out (power 1.2)
    • Lifetime: 0.6-1.2 seconds
  • Bubble: Rising with lateral wobble
    • Wobble frequency: direction * 4.0
    • Drift pattern: sin(angle + t * wobbleFreq) * radius
    • Lifetime: 1.2-2.5 seconds
  • Shimmer: Surface sparkle with twinkle effect
    • Fast twinkle using global time: sin(time8 + angle5) * sin(time13 + angle3)
    • Circular wander pattern on water surface
    • Lifetime: 1.5-3.0 seconds
  • Ripple: Expanding ring with fade
    • Phase-based scale: 0.15 + phase * 1.3
    • Early fade (0-15%): linear ramp to 0.55 opacity
    • Late fade (15-100%): power 1.5 decay from 0.55 to 0
    • Continuous expansion driven by time uniform
Fishing Spot Variants:
  • Net: Calm, gentle ripples (4 splash, 3 bubble, 3 shimmer, 2 ripples, burst every 5-10s)
  • Bait: Medium activity (5 splash, 4 bubble, 4 shimmer, 2 ripples, burst every 3-7s)
  • Fly: Active river fishing (8 splash, 5 bubble, 5 shimmer, 2 ripples, burst every 2-5s)
Burst System:
  • Fish activity bursts fire 2-4 splash particles simultaneously
  • Burst center randomized within 0.05-0.15 radius
  • Burst particles have higher peak heights (0.25-0.6 vs 0.12-0.32)
  • Burst particles cluster around burst center with 0.06 spread
  • Burst timer resets to random interval after each burst

Usage Example

import { ParticleManager } from '@hyperscape/shared';

// ResourceSystem creates ParticleManager on client startup
const particleManager = new ParticleManager(scene);

// Register water particles (fishing spot)
particleManager.registerSpot({
  entityId: 'entity-123',
  position: { x: 10, y: 0, z: 20 },
  resourceType: 'fishing_spot',
  resourceId: 'fishing_spot_net'
});

// Move existing spot (when fishing spot relocates)
particleManager.moveSpot('entity-123', 'fishing_spot', { x: 12, y: 0, z: 22 });

// Per-frame update (called by ResourceSystem)
particleManager.update(deltaTime, camera);

// Cleanup on entity destroy
particleManager.unregisterSpot('entity-123', 'fishing_spot');

// Dispose all particle managers on system shutdown
particleManager.dispose();

Integration with ResourceSystem

The ParticleManager is created and managed by ResourceSystem on the client:
// From packages/shared/src/systems/shared/entities/ResourceSystem.ts

// CLIENT: Create centralized particle hub for all particle effects
if (!this.world.isServer) {
  const scene = this.world.stage?.scene;
  if (scene) {
    this.particleManager = new ParticleManager(scene as any);

    // Retroactively register any fishing spot entities created before this system started
    const existingEntities = this.world.entities?.getByType?.("resource") || [];
    for (const entity of existingEntities) {
      if (
        entity instanceof ResourceEntity &&
        entity.config?.resourceType === "fishing_spot"
      ) {
        entity.tryRegisterWithParticleManager();
      }
    }
  }

  // Listen for resource events and route them through the particle hub
  this.subscribe(
    EventType.RESOURCE_SPAWNED,
    (data: { id?: string; type?: string; position?: { x: number; y: number; z: number } }) => {
      this.particleManager?.handleResourceEvent(data);
    }
  );
}

// Per-frame update: drives all particle managers on the client
update(dt: number): void {
  if (this.particleManager) {
    const camera = this.world.camera;
    if (camera) {
      this.particleManager.update(dt, camera);
    }
  }
}

Lazy Registration Pattern

ResourceEntity uses a lazy registration pattern to handle timing issues where entities may be created before ResourceSystem starts:
// From packages/shared/src/entities/world/ResourceEntity.ts

private _registeredWithParticleManager = false;

public tryRegisterWithParticleManager(): boolean {
  if (this._registeredWithParticleManager) return true;

  const pm = this.getParticleManager();
  if (!pm) return false;

  const pos = this.getPosition();
  pm.registerSpot({
    entityId: this.id,
    position: { x: pos.x, y: pos.y, z: pos.z },
    resourceType: this.config.resourceType || "",
    resourceId: this.config.resourceId || "",
  });
  this._registeredWithParticleManager = true;
  return true;
}

protected clientUpdate(_deltaTime: number): void {
  super.clientUpdate(_deltaTime);

  // Lazy registration: retry if particle manager wasn't ready during createFishingSpotVisual
  if (
    !this._registeredWithParticleManager &&
    this.config.resourceType === "fishing_spot"
  ) {
    if (this.tryRegisterWithParticleManager()) {
      console.log(`[FishingSpot] Late registration succeeded for ${this.id}`);
    }
  }

  // Organic glow pulse — two frequencies layered for natural breathing
  if (this.glowMesh) {
    const now = Date.now();
    const slow = Math.sin(now * 0.0015) * 0.04;
    const fast = Math.sin(now * 0.004 + 1.3) * 0.02;
    const pulse = 0.18 + slow + fast;
    (this.glowMesh.material as THREE.MeshBasicMaterial).opacity = pulse;
  }
}

Extensibility Guide

To add new particle types (fire, magic, dust, blood):
  1. Create Sub-Manager Class: Create new file in packages/shared/src/entities/managers/particleManager/
    // Example: MagicParticleManager.ts
    export class MagicParticleManager {
      constructor(scene: THREE.Scene) { /* ... */ }
      registerSpot(config: MagicSpotConfig): void { /* ... */ }
      unregisterSpot(entityId: string): void { /* ... */ }
      update(dt: number, camera: THREE.Camera): void { /* ... */ }
      dispose(): void { /* ... */ }
    }
    
  2. Instantiate in ParticleManager: Add to constructor
    export class ParticleManager {
      private waterManager: WaterParticleManager;
      private magicManager: MagicParticleManager; // Add new manager
    
      constructor(scene: THREE.Scene) {
        this.waterManager = new WaterParticleManager(scene);
        this.magicManager = new MagicParticleManager(scene); // Instantiate
      }
    }
    
  3. Add Routing Logic: Update register/unregister/move/handleEvent methods
    registerSpot(config: ParticleSpotConfig): void {
      if (this.isWaterType(config.resourceType)) {
        this.waterManager.registerSpot({ /* ... */ });
        return;
      }
      if (this.isMagicType(config.resourceType)) {
        this.magicManager.registerSpot({ /* ... */ });
        return;
      }
    }
    
  4. Call update() and dispose(): Add to ParticleManager lifecycle methods
    update(dt: number, camera: THREE.Camera): void {
      this.waterManager.update(dt, camera);
      this.magicManager.update(dt, camera); // Add update call
    }
    
    dispose(): void {
      this.waterManager.dispose();
      this.magicManager.dispose(); // Add dispose call
    }
    

Rendering System

WebGPU-Only Architecture

As of February 2026, all rendering uses WebGPU exclusively:
// From utils/rendering/RendererFactory.ts
export type RendererBackend = "webgpu";  // WebGL removed
export type UniversalRenderer = WebGPURenderer;

// Create WebGPU renderer (no fallback)
export async function createRenderer(options?: RendererOptions): Promise<WebGPURenderer> {
  if (!navigator.gpu) {
    throw new Error('WebGPU not supported. Please use Chrome 113+, Edge 113+, or Safari 18+');
  }
  
  const renderer = new THREE.WebGPURenderer({
    antialias: options?.antialias ?? true,
    powerPreference: options?.powerPreference ?? "high-performance",
  });
  
  await renderer.init();
  return renderer;
}
Breaking Change (Commit 47782ed, Feb 2026):
  • Removed all WebGL detection and fallback code
  • Removed isWebGLAvailable(), isWebGLForced(), canTransferCanvas() functions
  • RendererBackend type is now only "webgpu" (removed "webgl")
  • All shaders use TSL (Three Shading Language) which only compiles to WebGPU

Key Exports

Entity Hierarchy

The entity system follows a clear inheritance pattern:
// From packages/shared/src/entities/Entity.ts (lines 26-38)
// Entity (base class)
// ├── InteractableEntity (can be interacted with)
// │   ├── ResourceEntity (trees, rocks, fishing spots)
// │   ├── ItemEntity (ground items)
// │   └── NPCEntity (dialogue, shops)
// ├── CombatantEntity (can fight)
// │   ├── PlayerEntity (base player)
// │   │   ├── PlayerLocal (client-side local player)
// │   │   └── PlayerRemote (client-side remote players)
// │   └── MobEntity (enemies)
// └── HeadstoneEntity (player death markers)
ExportPurpose
EntityBase entity class with components, physics, networking
CombatantEntityCombat-capable entities with health, damage
InteractableEntityObjects players can interact with
PlayerEntityServer-side player with full stats, inventory
PlayerLocalClient-side local player with input handling
PlayerRemoteClient-side representation of other players
MobEntityNPCs and hostile mobs with AI states
HeadstoneEntityDeath marker for item retrieval

Systems (Shared)

Located in src/systems/shared/:
CategorySystems
character/Character stats, equipment, skills
combat/Combat mechanics, damage, death
death/Death handling, respawn
economy/Banks, shops, trading
entities/Entity lifecycle management
interaction/Player-world interactions
movement/Tile-based movement, collision, pathfinding
presentation/Visual rendering
world/World management

Collision System

The movement system includes a unified collision matrix for OSRS-accurate tile blocking:
// From packages/shared/src/systems/shared/movement/
export { CollisionMatrix } from './CollisionMatrix';
export { CollisionFlag, CollisionMask } from './CollisionFlags';
export { EntityOccupancyMap } from './EntityOccupancyMap';
export { TileSystem } from './TileSystem';
Key Exports:
ExportPurpose
CollisionMatrixZone-based collision storage (8×8 tile zones)
CollisionFlagBitmask flags (BLOCKED, WATER, OCCUPIED, walls)
CollisionMaskCombined masks (BLOCKS_WALK, BLOCKS_MOVEMENT)
EntityOccupancyMapEntity tracking facade over CollisionMatrix
tilesWithinRangeOfFootprintMulti-tile interaction range checks
Usage:
import { CollisionMatrix, CollisionMask } from '@hyperscape/shared';

const collision = new CollisionMatrix();

// Check if tile is walkable
if (!collision.hasFlags(x, z, CollisionMask.BLOCKS_WALK)) {
  // Safe to move
}

// Add blocking for a tree
collision.addFlags(x, z, CollisionFlag.BLOCKED);

Data Manifests

Game content is defined in TypeScript files in src/data/:
FilePurpose
DataManager.tsRuntime loader for manifests
npcs.tsNPC/mob definitions with drops, stats
items.tsItem definitions with stats, requirements
world-areas.tsZone configuration
banks-stores.tsShop inventories and bank locations
avatars.tsVRM avatar options
playerEmotes.tsEmote animation URLs
world-structure.tsWorld layout and spawn points
equipment-stats.jsonEquipment bonuses
equipment-requirements.jsonLevel requirements
NPC data is loaded from JSON manifests at runtime by DataManager. Add new NPCs in world/assets/manifests/npcs.json.

Entry Points

Server (index.ts)

Exports for server-side usage:
import { 
  Entity, 
  DataManager, 
  PlayerEntity,
  MobEntity 
} from '@hyperscape/shared';

Client (index.client.ts)

Exports for client-side usage:
import { 
  Entity, 
  PlayerLocal,
  PlayerRemote 
} from '@hyperscape/shared/client';

Dependencies

PackageVersionPurpose
three0.183.13D rendering
three-mesh-bvh0.9.8BVH raycasting optimization
@pixiv/three-vrm3.4.3VRM avatar support
@hyperscape/physx-js-webidlworkspacePhysics
@hyperscape/procgenworkspace (devDep)Procedural generation (type resolution only)
react^19.2.0UI components
styled-components^6.1.19CSS-in-JS
fastify^5.0.0HTTP server (server builds)
livekit-client^2.9.9Voice chat
zod4.3.6Schema validation
dotenv17.3.1Environment variables
Procgen Dependency: The @hyperscape/procgen package is listed as a devDependency (not a regular dependency) to break the circular dependency cycle. This allows TypeScript to find module declarations during type checking without creating a build cycle in Turbo. The import still resolves at runtime since both packages are always installed together in the workspace.Commits: f355276, 3b9c0f2, 05c2892 (Feb 25-26, 2026)

Building

cd packages/shared
bun run build    # Production build
bun run dev      # Watch mode with auto-rebuild
Shared must be built before other packages that depend on it. Turbo handles this automatically.

Key Patterns

ECS Architecture

All game logic uses Entity Component System. See ECS Concepts.

Manifest-Driven Data

Game content defined in src/data/. See Manifests.

Type Safety

Strong TypeScript typing throughout—no any types allowed. ESLint enforces this rule.

Dual Entry Points

  • index.ts: Server-side exports (includes Fastify, database utilities)
  • index.client.ts: Client-side exports (browser-compatible)