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
renderEntityor entity clipping artifacts - Marketplace plugin installation/validation issues
Do NOT use for:
- Modifying globe rendering primitives → see
cesium-renderingrule - Zustand state management → see
state-managementrule - Prisma schema changes → see
database-migrationsrule - Monorepo/pnpm workspace issues → see
monorepo-workflowrule
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.
- Scaffold: Run
node packages/wwv-cli/dist/index.js create <name> --localfrom the project root. This generates a boilerplate plugin inlocal-plugins/wwv-plugin-<name>. - Develop: Run
pnpm dev. The built-in file watcher (pnpm dev:plugins) automatically rebuilds and syncs your local plugin topublic/plugins-local/whenever you save a file for instant hot-reloading. - 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:
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:
{
"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/outlineWidthon a billboard entity causes GPU clipping artifacts. This is a hard rendering constraint.
Use createSvgIconUrl() from the SDK for billboard icons:
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.
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:
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):
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):
InstalledPluginsLoader reads from 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:
transpilePackages: [
"@worldwideview/wwv-plugin-sdk",
"@worldwideview/wwv-plugin-wildfires", // ADD
],
Add to tsconfig.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/tolocal-plugins/, use themigrate-legacy-pluginskill 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 schedulingsrc/redis.ts— Redis client,setLiveSnapshot()andgetLiveSnapshot()src/websocket.ts— WS handler,broadcastPluginData()src/seed-utils.ts—withRetry(),fetchWithTimeout(),haversineKm()src/db.ts— SQLite (better-sqlite3) for local history storagesrc/seeders/index.ts— Auto-discovery, dynamically requires all seeder files
2.2 Two Seeder Patterns
Cron Seeder (periodic polling — most common):
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(
"INSERT OR IGNORE INTO my_data (id, payload, source_ts, fetched_at) VALUES (@id, @payload, @source_ts, @fetched_at)"
);
async function seedMyPlugin() {
console.log("[MyPlugin] Polling external API...");
const res = await withRetry(() =>
fetchWithTimeout("https://api.example.com/data")
);
const data = await res.json();
const items = data.features.map((f: any) => ({
id: f.id,
lat: f.geometry.coordinates[1],
lon: f.geometry.coordinates[0],
name: f.properties.name,
// ... transform to your schema
}));
// Optional: save to SQLite
for (const item of items) {
insertRow.run({
id: item.id,
payload: JSON.stringify(item),
source_ts: Date.now(),
fetched_at: Date.now(),
});
}
// REQUIRED: Push to Redis + broadcast to WS subscribers
await setLiveSnapshot("my-plugin", {
source: "my-plugin",
fetchedAt: new Date().toISOString(),
items,
totalCount: items.length,
}, 3600); // TTL in seconds
}
registerSeeder({
name: "my-plugin", // MUST match the plugin's id on the frontend
cron: "*/15 * * * *", // Every 15 minutes
fn: seedMyPlugin,
});
Init Seeder (persistent/high-frequency — e.g., ISS every 5s):
import { setLiveSnapshot } from "../redis";
import { registerSeeder } from "../scheduler";
function startMySeeder() {
console.log("[MyPlugin] Starting persistent seeder...");
async function poll() {
const res = await fetch("https://api.example.com/live");
const data = await res.json();
await setLiveSnapshot("my-plugin", data, 60);
}
poll(); // Initial fetch
setInterval(poll, 5000); // Every 5 seconds
}
registerSeeder({
name: "my-plugin",
init: startMySeeder, // No cron — uses init for persistent polling
});
2.3 SeederDefinition Interface
interface SeederDefinition {
name: string; // Plugin ID — MUST match frontend plugin id
cron?: string; // Cron expression (e.g., "0 * * * *" = hourly)
fn?: () => Promise<void>; // Function called on each cron tick
init?: () => void; // Initialization for persistent seeders (websockets, intervals)
}
2.4 Auto-Discovery & Dependencies
The V2 engine dynamically import()s every compiled .mjs file in the extracted seeder workspaces. You do not need to manually import your seeder — just create the file and call registerSeeder() at module scope.
[!IMPORTANT] V2 Engine Dependencies: Seeders run in the unified V2 host environment which provides common packages (
zod,ws,undici, etc.). You MUST NOT bundle these dependencies in your seeder'sdistfolder. Leave them externalized in your build config.
2.5 SQLite History (Optional)
For data that benefits from historical queries (playback mode), add a table in src/db.ts:
db.exec(`
CREATE TABLE IF NOT EXISTS my_data (
id TEXT PRIMARY KEY,
payload JSON NOT NULL,
source_ts INTEGER NOT NULL,
fetched_at INTEGER NOT NULL
)
`);
2.6 Utility Functions
| Function | Purpose |
|---|---|
withRetry(fn, maxRetries, delayMs) |
Exponential backoff retry wrapper |
fetchWithTimeout(url, options, timeoutMs) |
Fetch with abort signal (default 15s) |
haversineKm(lat1, lon1, lat2, lon2) |
Distance between coordinates in km |
sleep(ms) |
Promise-based delay |
CHROME_UA |
Chrome User-Agent string for scraping |
Part 3: Data Pipeline (Frontend ↔ Engine)
3.1 WebSocket Protocol
Client → Engine:
{ "action": "subscribe", "pluginId": "earthquakes" }
{ "action": "unsubscribe", "pluginId": "earthquakes" }
Engine → Client:
{ "type": "welcome", "engine": "wwv-data-engine-v2", "plugins": ["earthquakes", "wildfires", ...] }
{ "type": "data", "pluginId": "earthquakes", "payload": { "items": [...], "totalCount": 42 } }
On subscribe, the engine immediately pushes the latest cached Redis snapshot — no waiting for the next cron tick.
3.2 Split-Routing (resolveEngineUrl)
resolveEngineUrl(pluginId) determines which engine serves each plugin:
- Local engine (
localhost:5000) if running and its/manifestincludes this plugin ← HIGHEST PRIORITY - Plugin's
getServerConfig().streamUrl(code override) - Plugin's
PluginManifest.dataSource.streamUrl(manifest override) NEXT_PUBLIC_WWV_PLUGIN_DATA_ENGINE_URLenv var- Fallback:
wss://dataenginev2.worldwideview.dev/stream(cloud)
3.3 Full Data Flow
Engine seeder calls setLiveSnapshot(pluginId, payload, ttl)
→ broadcastPluginData() fans out to subscribed WS connections
→ WsClient receives { type: "data", pluginId, payload }
→ WsClient calls plugin.mapWebsocketPayload() if defined
→ DataBus.emit("dataUpdated", { pluginId, entities })
→ DataBusSubscriber → Zustand store.entitiesByPlugin
→ GlobeView (memoized visible entities)
→ EntityRenderer (billboard/point/label primitives)
3.4 Host Globals (CDN Plugins)
CDN-loaded plugins share the host app's dependencies via globalThis.__WWV_HOST__:
react,react-dom,react/jsx-runtimecesium,resiumzustand@worldwideview/wwv-plugin-sdk
Use wwvPluginGlobals() in your Vite config to externalize automatically:
import { defineConfig } from "vite";
import { wwvPluginGlobals } from "@worldwideview/wwv-plugin-sdk";
export default defineConfig({
plugins: [wwvPluginGlobals()],
build: {
lib: { entry: "./src/index.ts", formats: ["es"], fileName: "frontend" },
rollupOptions: { output: { entryFileNames: "frontend.mjs" } },
},
});
Part 4: Static GeoJSON Plugins
For plugins with static data (no live API), use wwvStaticCompiler:
import { defineConfig } from "vite";
import { wwvPluginGlobals, wwvStaticCompiler } from "@worldwideview/wwv-plugin-sdk";
export default defineConfig({
plugins: [wwvStaticCompiler(), wwvPluginGlobals()],
build: {
lib: { entry: "./src/index.ts", formats: ["es"], fileName: "frontend" },
},
});
Place GeoJSON in data/data.json. The compiler auto-generates a WorldPlugin class from package.json metadata at build time. No src/index.ts needed.
Part 5: Example - Simple ISS Tracker Plugin
Here is a minimal, complete example of a live ISS Tracker plugin utilizing both the frontend SDK and the V2 Engine.
1. Frontend Plugin (local-plugins/wwv-plugin-iss/src/index.ts)
import type {
WorldPlugin, PluginContext, GeoEntity,
CesiumEntityOptions, TimeRange, LayerConfig, PluginCategory
} from "@worldwideview/wwv-plugin-sdk";
import { Satellite } from "lucide-react";
import pkg from "../package.json";
export default class IssPlugin implements WorldPlugin {
id = "iss";
name = "ISS Tracker";
description = "Real-time International Space Station tracking";
icon = Satellite;
category: PluginCategory = "space";
version = pkg.version;
async initialize(ctx: PluginContext): Promise<void> {}
destroy(): void {}
async fetch(_timeRange: TimeRange): Promise<GeoEntity[]> {
return []; // WS-only — data arrives via mapWebsocketPayload
}
getPollingInterval(): number {
return 0; // WS-only
}
// REQUIRED: seeder sends { source, fetchedAt, items: [...], totalCount } (an object).
// Without this, WsClient drops the payload silently — the globe stays empty.
mapWebsocketPayload(payload: any, _existingEntities: GeoEntity[]): GeoEntity[] {
const items: any[] = payload?.items ?? (Array.isArray(payload) ? payload : []);
return items.map((item: any): GeoEntity => ({
id: `iss-${item.id}`,
pluginId: "iss",
latitude: item.lat,
longitude: item.lon,
altitude: item.alt ?? 0,
timestamp: new Date(),
label: "International Space Station",
properties: { speed: item.speed },
}));
}
getLayerConfig(): LayerConfig {
return {
color: "#ffffff",
clusterEnabled: false,
maxEntities: 1,
};
}
renderEntity(entity: GeoEntity): CesiumEntityOptions {
return {
type: "point",
color: "#ffffff",
size: 10,
outlineColor: "#3b82f6",
outlineWidth: 2,
};
}
}
2. Backend Init Seeder (wwv-seeders/src/seeders/iss.ts)
import { setLiveSnapshot } from "../redis";
import { registerSeeder } from "../scheduler";
function startIssSeeder() {
console.log("[ISS] Starting live tracker...");
async function poll() {
try {
const res = await fetch("https://api.wheretheiss.at/v1/satellites/25544");
const data = await res.json();
const item = {
id: "iss",
lat: data.latitude,
lon: data.longitude,
alt: data.altitude * 1000, // Convert km to meters
name: "International Space Station",
speed: data.velocity,
};
await setLiveSnapshot("iss", {
source: "iss",
fetchedAt: new Date().toISOString(),
items: [item],
totalCount: 1,
}, 10);
} catch (err) {
console.error("[ISS] Fetch failed", err);
}
}
poll(); // Initial fetch
setInterval(poll, 5000); // Poll every 5 seconds
}
registerSeeder({
name: "iss",
init: startIssSeeder,
});
Common Mistakes
"Sandbox Plugin Breaking Next.js Build"
- Premature Configuration — Modifying
next.config.ts(transpilePackages) ortsconfig.json(paths) while a plugin is still inlocal-plugins/. Thelocal-pluginssandbox relies on dynamic hot-loading in the browser viapnpm dev:plugins. Do not touch monorepo configs for local plugins. - Manual Registration — Manually adding a sandbox plugin to
AppShell.tsx. Let the local hot-loading manifest handle sandbox plugins dynamically.
"Missing Boilerplate / Errors in package.json"
- Bypassing the CLI — Creating a plugin manually instead of using
node packages/wwv-cli/dist/index.js create <name> --local. Always use the CLI to ensure the correct metadata, type definitions, and test scaffolding are generated.
"No data on the globe"
- Seeder name ≠ plugin id — The
nameinregisterSeeder()MUST match theidproperty of the frontendWorldPluginclass - Engine
/manifestmissing plugin — CheckGET localhost:5000/manifest→pluginsarray - WsClient not subscribing — Add
console.loginresolveEngineUrl()to verify URL resolution - Plugin hardcodes engine URL — Legacy plugins use
this.context?.apiBaseUrlinstead ofresolveEngineUrl(). Seemigrate-legacy-pluginskill mapWebsocketPayloadmissing — If the engine sends a non-standard payload shape, the plugin must implementmapWebsocketPayload()or data silently drops
"Build crash / Module not found"
- Missing
transpilePackagesentry innext.config.ts→ immediate runtime crash - Missing
tsconfig.jsonpath alias → TypeScript compilation error - Forgot
pnpm installafter creating package → dependency resolution failure
"Invalid Hook Call" (CDN plugins)
Plugin bundles its own React instead of using host globals. Fix: add wwvPluginGlobals() to Vite config.
"Manifest validation failed"
Required fields: id, name, version, entry, trust. Entry URL must be from allowed domains (CDN, localhost, worldwideview.dev, or relative path). capabilities must be non-empty array.
"GPU clipping / entity disappearing"
Using size/outlineWidth/outlineColor on a "billboard" entity. These properties are point-only. Billboard uses iconUrl/iconScale/color.
"Redis snapshot not updating"
setLiveSnapshot() throttles Redis writes to every 5 minutes to avoid exceeding Upstash request limits. WebSocket broadcasts are not throttled. Check WS delivery first.
"Category type error"
Categories are lowercase: "aviation" not "Aviation", "natural-disaster" not "NaturalDisaster". Must match the PluginCategory union in the SDK.
End-to-End Checklist
- Development: Scaffolded via
node packages/wwv-cli/dist/index.js createand developed inlocal-plugins/ - Publishing: Published to NPM via
node packages/wwv-cli/dist/index.js publish <name> [--org <your-org>](if making public) - Frontend: Plugin class implements
WorldPluginwith correctid - Frontend:
package.jsonhasworldwideviewblock with matchingid - Frontend: Version imported from
package.json(never hardcoded) - Frontend: Added to
transpilePackagesinnext.config.ts - Frontend: Added path alias in
tsconfig.json - Frontend: Registered via
pluginRegistry+pluginManagerinAppShell.tsx - Frontend:
renderEntity()doesn't mix point/billboard properties - Frontend: Date/time fields wrapped with
dtProp() - Frontend: External links wrapped with
urlProp() - Frontend: Image URLs wrapped with
imageProp() - Frontend: Video/stream URLs wrapped with
videoProp() - Engine: Seeder file in
wwv-seeders/src/seeders/<name>.ts(or private repo) - Engine:
registerSeeder({ name })matches frontend pluginid - Engine: Calls
setLiveSnapshot()with correct plugin ID key - Engine: Seeder payload is either a flat
GeoEntity[]array OR plugin implementsmapWebsocketPayload(any other object is silently dropped by WsClient) - Frontend (WS-only):
getPollingInterval()returns0andfetch()returns[] - Frontend (WS-only):
mapWebsocketPayload(payload, existingEntities)implemented if seeder sends an object - Engine: SQLite table created in
db.tsif historical data needed - Verification:
GET localhost:5000/manifestincludes the plugin ID - Verification:
GET localhost:5000/healthshows seeder last-run timestamp - Verification: WebSocket subscribe returns data snapshot
- Workspace: Ran
pnpm installfrom project root - Workspace: Version bump follows
/commitworkflow