- name
- worldwideview-plugin-creation
- description
- Use when creating a new plugin, adding a new data source, or debugging missing plugin data in the WorldWideView project. Triggers on seeder creation, WebSocket streaming issues, plugin registration failures, manifest validation errors, GeoEntity rendering problems, or data engine integration
# WorldWideView Plugin Creation
## Overview
A WorldWideView plugin is a self-contained data source that renders geospatial entities on a 3D Cesium globe. Every plugin implements the `WorldPlugin` interface from the SDK and connects to the **V2 Data Engine** (Fastify + Redis) that streams live data over WebSocket.
**Core principle:** Plugins are code bundles. The engine pushes data. The frontend renders it. There is no REST polling between frontend and engine — only WebSocket streaming.
## When to Use
- Creating a new data layer plugin (any architecture)
- Building a new seeder in the data engine
- Plugin data doesn't appear on the globe
- WebSocket subscription or split-routing issues
- Build failures after adding a plugin package
- Debugging `renderEntity` or entity clipping artifacts
- Marketplace plugin installation/validation issues
**Do NOT use for:**
- Modifying globe rendering primitives → see `cesium-rendering` rule
- Zustand state management → see `state-management` rule
- Prisma schema changes → see `database-migrations` rule
- Monorepo/pnpm workspace issues → see `monorepo-workflow` rule
## Architecture Decision
```
Does it need live external API data?
├── NO → Static GeoJSON plugin (wwvStaticCompiler in vite.config.ts)
│ No seeder needed. Place data in data/data.json.
└── YES → Does it update more often than every 30s?
├── NO → Cron Seeder (engine, registerSeeder with cron string)
│ getPollingInterval() = 0, mapWebsocketPayload() required
└── YES → Init Seeder (engine, registerSeeder with init function)
getPollingInterval() = 0, mapWebsocketPayload() required
```
---
## Quick Reference
| Piece | Location | Purpose |
|---|---|---|
| Sandbox plugin | `local-plugins/wwv-plugin-<name>/` | Local development workspace for new plugins |
| Plugin class | `packages/wwv-plugin-<name>/src/index.ts` | Implements `WorldPlugin` interface |
| Package metadata | `packages/wwv-plugin-<name>/package.json` | `worldwideview` block with id, type, format, category |
| Engine seeder | `wwv-seeders/src/seeders/<name>.ts` | Fetches external data → Redis → WS broadcast |
| Build config | `next.config.ts` → `transpilePackages` | Required for monorepo packages |
| TS alias | `tsconfig.json` → `paths` | Required for monorepo packages |
| Registration | `src/core/plugins/PluginRegistry.ts` | Register plugin at boot |
| WS client | `src/core/data/WsClient.ts` | Client-side WebSocket subscriptions |
| URL resolution | `src/core/data/resolveEngineUrl.ts` | Per-plugin local vs cloud routing |
| Manifest validation | `src/core/plugins/validateManifest.ts` | Checks required fields for CDN bundles |
| Host globals | `src/core/plugins/hostGlobals.ts` | Injects shared deps for CDN plugins |
---
## Part 1: Frontend Plugin (WorldPlugin)
### 1.0 Development Workflow (Local Sandbox)
The standard way to develop a new plugin is using the **Local Sandbox** (`local-plugins/` directory). This keeps experimental code out of the main monorepo packages until it is ready for production.
1. **Scaffold**: Run `node packages/wwv-cli/dist/index.js create <name> --local` from the project root. This generates a boilerplate plugin in `local-plugins/wwv-plugin-<name>`.
2. **Develop**: Run `pnpm dev`. The built-in file watcher (`pnpm dev:plugins`) automatically rebuilds and syncs your local plugin to `public/plugins-local/` whenever you save a file for instant hot-reloading.
3. **Publish (Optional)**: When stable, you can use `node packages/wwv-cli/dist/index.js publish <name>` from the project root to publish the plugin to NPM (use the `--org <your-org>` flag to publish under your own NPM organization instead of `@worldwideview`). There is no need to 'link' plugins to the core monorepo manually, as the local sandbox runs natively inside the workspace.
### 1.1 Implement the Interface
Every plugin **must** implement `WorldPlugin` from `@worldwideview/wwv-plugin-sdk`:
```typescript
import type {
WorldPlugin, PluginContext, GeoEntity,
CesiumEntityOptions, TimeRange, LayerConfig, PluginCategory
} from "@worldwideview/wwv-plugin-sdk";
import { Flame } from "lucide-react";
import pkg from "../package.json";
export default class WildfiresPlugin implements WorldPlugin {
id = "wildfires";
name = "Wildfires";
description = "Live wildfire incidents from NASA FIRMS";
icon = Flame;
category: PluginCategory = "natural-disaster";
version = pkg.version; // NEVER hardcode — always import from package.json
async initialize(ctx: PluginContext): Promise<void> {
// Store context for later use (env vars, edition, callbacks)
}
destroy(): void {
// Cleanup timers, WebSocket connections, etc.
}
async fetch(timeRange: TimeRange): Promise<GeoEntity[]> {
// For WS-streamed plugins, return [] — data arrives via WsClient
return [];
}
getPollingInterval(): number {
return 0; // 0 = no REST polling (WS-only)
}
getLayerConfig(): LayerConfig {
return {
color: "#ef4444",
clusterEnabled: true,
clusterDistance: 50,
maxEntities: 5000,
};
}
renderEntity(entity: GeoEntity): CesiumEntityOptions {
return {
type: "point",
color: "#ef4444",
size: 6,
outlineColor: "#ffffff",
outlineWidth: 1,
};
}
}
```
### 1.2 Required vs Optional Methods
**Required:**
| Method | Purpose |
|---|---|
| `initialize(ctx)` | Receive `PluginContext` (env, edition, callbacks) |
| `destroy()` | Cleanup on shutdown |
| `fetch(timeRange)` | Return entities (REST path). Return `[]` for WS-only |
| `getPollingInterval()` | Polling frequency in ms. Return `0` for WS-only |
| `getLayerConfig()` | Layer appearance (color, clustering, max entities) |
| `renderEntity(entity)` | Per-entity Cesium rendering options |
**Optional (but `mapWebsocketPayload` is REQUIRED for WS-only plugins with object payloads):**
| Method | Purpose |
|---|---|
| `getServerConfig()` | REST/WS endpoint config (`apiBasePath`, `streamUrl`) |
| `getSelectionBehavior(entity)` | Trail/fly-to on entity click |
| `getFilterDefinitions()` | User-facing filter controls |
| `getLegend()` | Legend entries for layer panel |
| `getSidebarComponent()` | Custom sidebar React component |
| `getDetailComponent()` | Custom detail panel for selected entity |
| `getSettingsComponent()` | Plugin settings UI |
| `getGlobeComponent()` | React component injected into globe view |
| `mapWebsocketPayload(payload, existing)` | **Required if seeder payload is an object** (not a flat array). Transforms raw engine payload → `GeoEntity[]`. Without it, `WsClient` drops object payloads silently. |
| `requiresConfiguration(settings)` | Return `true` if plugin needs setup first |
### 1.3 Package Metadata
The `package.json` **must** include a `worldwideview` block:
```json
{
"name": "@worldwideview/wwv-plugin-wildfires",
"version": "1.0.0",
"main": "src/index.ts",
"worldwideview": {
"id": "wildfires",
"type": "data-layer",
"format": "bundle",
"category": "natural-disaster",
"icon": "Flame",
"capabilities": ["data:own", "globe:overlay"]
},
"dependencies": {
"@worldwideview/wwv-plugin-sdk": "workspace:*"
}
}
```
**Valid categories** (lowercase only): `aviation` · `maritime` · `military` · `conflict` · `natural-disaster` · `infrastructure` · `space` · `cyber` · `economic` · `intelligence` · `custom`
**Valid capabilities**: `data:own` · `data:read:{plugin}` · `ui:detail-panel` · `ui:sidebar` · `ui:toolbar` · `ui:settings` · `globe:overlay` · `globe:camera` · `storage:read` · `storage:write` · `network:fetch`
### 1.4 Rendering Rules
| Entity Type | Properties | Notes |
|---|---|---|
| `"point"` | `color`, `size`, `outlineColor`, `outlineWidth` | Simple colored dots |
| `"billboard"` | `iconUrl`, `color`, `iconScale` | SVG/PNG icons |
| `"model"` | `modelUrl`, `modelScale`, `modelMinPixelSize` | 3D glTF models |
| `"label"` | `labelText`, `labelFont`, `color` | Text labels |
> **CRITICAL:** NEVER mix point and billboard properties. Using `size`/`outlineWidth` on a billboard entity causes GPU clipping artifacts. This is a hard rendering constraint.
Use `createSvgIconUrl()` from the SDK for billboard icons:
```typescript
import { createSvgIconUrl } from "@worldwideview/wwv-plugin-sdk";
import { Plane } from "lucide-react";
const iconUrl = createSvgIconUrl(Plane, { color: "#3b82f6" });
```
### 1.5 Property Tag Helpers
Use these SDK helpers to wrap property values so the Intel panel renders them richly. Without the tag prefix, the panel falls back to plain text.
```ts
import { dtProp, urlProp, imageProp, videoProp } from "@worldwideview/wwv-plugin-sdk";
```
| Helper | Tag format stored | Panel renders as |
|---|---|---|
| `dtProp(iso)` | `"datetime:2026-06-01T05:00:00Z"` | Expandable row: local time (collapsed), UTC + relative (expanded) |
| `urlProp(href)` | `"url:https://..."` | Clickable link with external icon |
| `imageProp(src)` | `"image:https://..."` | Inline thumbnail |
| `videoProp(href)` | `"video:https://..."` | "Watch" link with play icon |
All four helpers return `null` for empty, `null`, or `undefined` input — safe to call unconditionally.
**Usage pattern:**
```ts
properties: {
// dates — pass ISO 8601 strings
updated_at: dtProp(item.updated_at ?? null),
// URLs
source_url: urlProp(item.url ?? null),
// images and video
preview: imageProp(item.image_url ?? null),
stream: videoProp(item.video_url ?? null),
// plain values — no helper needed
name: item.name,
count: item.count,
}
```
### 1.6 Registration (3 Paths)
**A. Built-in (monorepo package):**
```typescript
import { pluginRegistry } from "@/core/plugins/PluginRegistry";
import { pluginManager } from "@/core/plugins/PluginManager";
import WildfiresPlugin from "@worldwideview/wwv-plugin-wildfires";
const plugin = new WildfiresPlugin();
pluginRegistry.register(plugin);
await pluginManager.registerPlugin(plugin);
```
`AppShell.tsx` iterates `pluginRegistry.getAll()` at startup.
**B. Marketplace (database-installed):**
The client hook `useMarketplaceSync` fetches manifests from `/api/marketplace/load` (backed by the `installed_plugins` PostgreSQL table) and calls `pluginManager.loadFromManifest()`. No code changes needed.
**C. Dynamic import (runtime):**
For user-imported GeoJSON layers, call `pluginManager.loadFromManifest(manifest)` directly.
### 1.7 Build Configuration (Core Monorepo Packages Only)
If you decide to manually move a plugin from the local sandbox into the core `packages/` directory, you must manually add it to the build pipeline:
Add to `next.config.ts`:
```typescript
transpilePackages: [
"@worldwideview/wwv-plugin-sdk",
"@worldwideview/wwv-plugin-wildfires", // ADD
],
```
Add to `tsconfig.json`:
```json
"paths": {
"@worldwideview/wwv-plugin-wildfires": ["./packages/wwv-plugin-wildfires/src"]
}
```
Run `pnpm install` from project root after creating the package.
---
## Part 2: Data Engine Seeder
The data engine is a **content-agnostic Host Environment runner** (`wwv-data-engine`, deployed via Docker). It runs Fastify on port 5000 with Redis caching and WebSocket streaming. Seeders are volume-mounted from `local-seeders/` (dev) or dynamically downloaded and linked via pnpm workspaces from GitHub Releases (`wwv-seeders` and `wwv-seeders-private`) in production.
> [!IMPORTANT]
> If you are **migrating** an existing plugin from `packages/` to `local-plugins/`, use the `migrate-legacy-plugin` skill instead. It covers both frontend routing fixes AND backend seeder build fixes as two independent concerns.
### 2.1 Engine Architecture
```
External API → Seeder (fetch + transform)
→ setLiveSnapshot(pluginId, payload, ttlSeconds)
→ broadcastPluginData() → Active WS subscribers (immediate)
→ Redis SET data:{pluginId}:live (throttled to every 5 min)
→ SQLite history (optional, via better-sqlite3)
```
**Key components:**
- `src/scheduler.ts` — Seeder registry and cron scheduling
- `src/redis.ts` — Redis client, `setLiveSnapshot()` and `getLiveSnapshot()`
- `src/websocket.ts` — WS handler, `broadcastPluginData()`
- `src/seed-utils.ts` — `withRetry()`, `fetchWithTimeout()`, `haversineKm()`
- `src/db.ts` — SQLite (better-sqlite3) for local history storage
- `src/seeders/index.ts` — Auto-discovery, dynamically requires all seeder files
### 2.2 Two Seeder Patterns
**Cron Seeder** (periodic polling — most common):
```typescript
import { db } from "../db";
import { setLiveSnapshot } from "../redis";
import { fetchWithTimeout, withRetry } from "../seed-utils";
import { registerSeeder } from "../scheduler";
// Optional: SQLite history table insert
const insertRow = db.prepare(
Voir sur GitHub