134c - Slow Render Server
Design Log #134c — Slow Render Server & Data Change
Written for AI agents. See Log Methodology Note below for details.
Date: May 10, 2026 Status: Draft Parent: #134 (production build) Related: #134a (build pipeline), #134b (main server), #134d (server build), #110 (slow render cache), #86 (workflow lifecycle)
Background
The slow render server is responsible for producing the build artifacts that the main server consumes. It runs in two modes:
- One-shot build (
jay-stack build) — builds everything for a new version, then exits. Used in CI/CD pipelines and deployments. - Long-running renderer (
jay-stack serve --role=renderer) — stays alive after the initial build, listening for data change webhooks and re-rendering affected instances.
The slow render server is the production equivalent of the dev server's handlePreRenderRequest() path + file watcher invalidation. The key difference: it runs the pipeline eagerly for all instances instead of lazily on first request.
Design
Build Mode (jay-stack build)
One-shot execution that produces a complete versioned bucket:
jay-stack build --version=2
│
├── Phase 0: Shared Artifacts (DL#134a, DL#134d)
│ ├── Scan routes + merge plugin routes
│ ├── Compile server code (Vite SSR build)
│ ├── Build shared client chunks
│ └── Discover all param combinations (loadParams)
│
├── Phase 1: Per-Instance Pipeline (concurrent)
│ For each (route, params):
│ ├── Slow render → pre-rendered jay-html
│ ├── Compile server element (esbuild)
│ ├── Generate hydration entry
│ └── Per-instance Vite build
│
├── Phase 2: Finalize
│ ├── Write route-manifest.json
│ └── Write build-metadata.json
│
└── Exit
Output: build/v2/ — a complete, self-contained bucket ready for deployment.
The version number is provided as a CLI argument or derived automatically (git commit hash, CI build number, incrementing counter). The build metadata records:
interface BuildMetadata {
version: string;
sourceHash: string; // Hash of source files at build time
buildTimestamp: string;
nodeVersion: string;
instanceCount: number;
}
Renderer Mode (jay-stack serve --role=renderer)
Long-running process that:
- On startup, verifies/builds the current version bucket
- Listens for data change webhooks
- Re-renders affected instances on data change
jay-stack serve --role=renderer --version=2
│
├── Startup
│ ├── Check build-metadata.json in build/v2/
│ ├── If source hash matches → skip rebuild, artifacts valid
│ ├── If source hash differs → full rebuild (Phase 0 + 1 + 2)
│ └── Initialize services (same as main server)
│
├── Start HTTP Server (webhook endpoints)
│ ├── POST /_jay/webhooks/:pluginName → data change handler
│ ├── POST /_jay/render/:route → manual re-render trigger
│ └── GET /_jay/status → health check + build info
│
└── Listen for webhooks...
Data Change Flow
External system (CMS, database, etc.)
│
├── Sends webhook to slow render server
│ POST /_jay/webhooks/wix-stores.product-change
│ Body: { "type": "product.updated", "itemId": "prod-123" }
│
├── Plugin webhook handler (makeWebhook) executes:
│ 1. Parses event, resolves itemId to slug
│ 2. Calls invalidate('product-page', { slug: 'blue-widget' })
│ (plugin invalidates by its own contract name, not route)
│
├── Framework resolves contract → routes:
│ Finds all routes using the 'product-page' contract
│ (could be /products/[slug], /shop/[id], etc.)
│
├── Framework invalidation runs per-instance pipeline:
│ 1. Slow render (with fresh data from plugin service)
│ 2. Compare new pre-rendered jay-html with existing
│ 3. If unchanged → skip remaining steps
│ 4. If changed → compile server element + hydration entry + Vite build
│ 5. Atomic update of route-manifest.json
│
└── Main server picks up changes on next request
(timestamp-based caching detects file changes)
Plugin Webhook Registration
Plugins register webhooks using a builder pattern similar to makeJayAction:
// In plugin's actions or init file
import { makeWebhook, type InvalidateContract } from '@jay-framework/fullstack-component';
export const onProductChange = makeWebhook(
'wix-stores.product-change',
async (req: HttpRequest, invalidate: InvalidateContract): Promise<HttpResponse> => {
const event = await req.json();
const { type, itemId } = event;
const slug = await resolveProductSlug(itemId);
// Re-render pages using 'product-page' contract with this specific slug
await invalidate('product-page', { slug });
// Re-render all pages using 'search-results' contract
// (any product change could affect search results)
await invalidate('search-results');
return { status: 200, body: { ok: true } };
},
);
The invalidate API is contract-based — plugins invalidate by their own contract name, not by route patterns. The framework resolves which routes use each contract.
type InvalidateContract = (contractName: string, params?: Record<string, string>) => Promise<void>;
invalidate('product-page', { slug: 'blue-widget' })— find all routes using theproduct-pagecontract, re-render the instance with matching params. If no instance exists (new product), build a new one.invalidate('search-results')— find all routes using thesearch-resultscontract, re-render all instances of those routes.
Why contract-based, not route-based: A plugin defines contracts, not routes. The plugin wix-stores knows it provides the product-page contract, but doesn't know (and shouldn't know) that the project mapped it to /products/[slug] or /shop/[id]. The framework knows the mapping because the route manifest tracks which contracts each route uses.
Future: reverse dependency map. A more fine-grained approach would track which data items each instance depends on during slow render (e.g., instance /search?q=shoes consumed products A, B, C). On product A change, only instances that consumed product A are re-rendered. This is an optimization that can be added later without changing the invalidate API — it would refine the paramless invalidate('search-results') to only rebuild affected instances instead of all.
Webhooks are automatically exposed at /_jay/webhooks/{webhookName}:
POST /_jay/webhooks/wix-stores.product-change→ callsonProductChange
Discovery works the same as actions — scan exports for JayWebhook-branded constants, register at startup. The webhook is a server-only construct (no client transform needed).
Invalidation Implementation
The invalidate function provided to webhook handlers enqueues rebuild requests rather than rebuilding directly. The webhook returns immediately; rebuilds happen asynchronously via the invalidation queue:
function createInvalidator(
queue: InvalidationQueue,
manifest: RouteManifest,
webhookName: string,
): InvalidateContract {
return async (contractName: string, params?: Record<string, string>) => {
// Resolve contract → affected routes
const affectedRoutes = manifest.routes.filter((r) => r.contracts?.includes(contractName));
for (const route of affectedRoutes) {
await queue.enqueue({
routePattern: route.pattern,
params,
source: webhookName,
timestamp: Date.now(),
});
}
};
}
The framework resolves contractName to routes using the contracts field in each RouteEntry. This field is populated during the build by scanning which contracts each page's jay-html references (headless component tags, keyed components).
Specific instance invalidation (invalidate('product-page', { slug: 'blue-widget' })):
async function rebuildInstance(
route: RouteEntry,
params: Record<string, string>,
ctx: BuildContext,
): Promise<void> {
const existing = route.instances.find((i) => paramsMatch(i.params, params));
// Run per-instance pipeline (slow render → compare → compile if changed)
const result = await buildInstance(route, params, ctx);
if (existing) {
// Update existing instance in manifest
Object.assign(existing, result);
} else {
// New instance (e.g., new product) — add to manifest
route.instances.push(result);
}
await atomicManifestUpdate(ctx);
}
Full contract invalidation (invalidate('search-results') — no params):
async function rebuildRoute(route: RouteEntry, ctx: BuildContext): Promise<void> {
// Re-run loadParams to discover current param combinations
const pageModule = await ctx.artifacts.loadPageModule(route.serverModule);
const currentParams = await collectLoadParams(pageModule);
// Find new, existing, and deleted instances
const existingParams = new Set(route.instances.map((i) => paramKey(i.params)));
const currentParamSet = new Set(currentParams.map((p) => paramKey(p)));
// Rebuild existing + new instances
for (const params of currentParams) {
await rebuildInstance(route, params, ctx);
}
// Remove deleted instances
route.instances = route.instances.filter((i) => currentParamSet.has(paramKey(i.params)));
await atomicManifestUpdate(ctx);
}
Optimistic Skip: Compare Pre-rendered Output
After re-running slow render, compare the new pre-rendered jay-html with the existing one. If they're identical (data didn't actually change, or changes don't affect the template), skip server element compilation and Vite build:
const newPreRendered = await preRenderJayHtml(jayHtml, slowViewState);
const existingContent = await fs.readFile(instance.preRenderedPath, 'utf-8');
if (stripCacheMetadata(newPreRendered) === stripCacheMetadata(existingContent)) {
// No structural change — skip compilation
// Still update cache metadata (new slowViewState/carryForward)
return;
}
// Structure changed — full rebuild of this instance
await compileServerElement(newPreRendered, ...);
await buildHydrationEntry(newPreRendered, ...);
await viteBuiltInstance(hydrateEntry, ...);
This is important because many data changes don't affect template structure (e.g., price change updates ViewState but doesn't toggle a slow conditional).
Startup Validation
On renderer startup, check if existing artifacts are valid:
async function validateExistingBuild(
buildDir: string,
sourceHash: string,
): Promise<'valid' | 'stale' | 'missing'> {
const metadataPath = path.join(buildDir, 'build-metadata.json');
try {
const metadata = JSON.parse(await fs.readFile(metadataPath, 'utf-8'));
if (metadata.sourceHash === sourceHash) {
return 'valid'; // Same code → artifacts are good
}
return 'stale'; // Code changed → need full rebuild
} catch {
return 'missing'; // No build exists
}
}
Source hash is computed from source files that affect the build:
src/pages/**/*.ts,src/pages/**/*.jay-html,src/pages/**/*.jay-contractsrc/actions/**/*.tssrc/lib/init.tspackage.json(dependency versions)
Concurrency and Load Management
The per-instance pipeline is CPU-intensive (slow render calls external APIs, esbuild compiles TS, Vite bundles JS). The renderer controls concurrency:
const CONCURRENCY = parseInt(process.env.JAY_BUILD_CONCURRENCY || '4');
async function buildAllInstances(routes: RouteEntry[], buildContext: BuildContext): Promise<void> {
const allInstances = routes.flatMap((route) =>
route.paramCombinations.map((params) => ({ route, params })),
);
// Bounded concurrency — process N instances at a time
const queue = [...allInstances];
const workers = Array.from({ length: CONCURRENCY }, async () => {
while (queue.length > 0) {
const item = queue.shift()!;
await buildInstance(item.route, item.params, buildContext);
}
});
await Promise.all(workers);
}
For data change re-renders, the concurrency is typically 1 (single instance rebuild), but batch webhooks (many products updated at once) benefit from parallelism.
Error Handling During Data Changes
If a re-render fails (slow render throws, Vite build fails, etc.), the slow render server:
- Keeps existing artifacts — the previous working version of the instance remains on disk
- Does not update the manifest — the main server continues serving the old instance
- Returns error in webhook response —
{ affected: 1, rebuilt: 0, errors: [{ params: {...}, error: "..." }] } - Logs the error for monitoring/alerting
This is safe because each instance is rebuilt independently. A failed rebuild of one instance doesn't affect other instances or the overall manifest.
Invalidation Queue
Webhook handlers don't rebuild directly — they enqueue invalidation requests. A worker pool drains the queue with bounded concurrency:
// Queue operates on routes (contract → route resolution happens in createInvalidator)
interface InvalidationRequest {
routePattern: string;
params?: Record<string, string>; // undefined = all instances
source: string; // webhook name, for logging
timestamp: number;
}
interface InvalidationQueue {
enqueue(request: InvalidationRequest): Promise<void>;
drain(): AsyncIterable<InvalidationRequest>;
depth(): number;
}
The queue provides:
- Deduplication — multiple invalidations for the same route+params within a short window collapse into one rebuild
- Bounded concurrency — N workers process queue items in parallel (configurable via
JAY_BUILD_CONCURRENCY) - Serialization per route — items for the same route are processed sequentially (prevents half-written artifacts)
- Backpressure — webhooks return immediately after enqueue; rebuild happens asynchronously
class InMemoryInvalidationQueue implements InvalidationQueue {
private pending = new Map<string, InvalidationRequest>(); // key = route+params
private processing = new Set<string>(); // routes currently rebuilding
private signal = new EventEmitter();
async enqueue(request: InvalidationRequest) {
const key = request.params
? `${request.routePattern}:${paramKey(request.params)}`
: request.routePattern;
// Dedup: newer request replaces older for same key
this.pending.set(key, request);
this.signal.emit('enqueued');
}
depth() {
return this.pending.size;
}
}
Initial implementation: In-memory queue (simple, no external dependencies). The queue interface is pluggable — can be swapped for a disk-based queue (survives restarts) or an infrastructure queue (Redis, SQS) for distributed renderer deployments.
Worker loop:
async function startWorkers(queue: InvalidationQueue, ctx: BuildContext) {
const concurrency = parseInt(process.env.JAY_BUILD_CONCURRENCY || '4');
for (let i = 0; i < concurrency; i++) {
(async () => {
for await (const request of queue.drain()) {
try {
if (request.params) {
await rebuildInstance(findRoute(request.routePattern), request.params, ctx);
} else {
await rebuildRoute(findRoute(request.routePattern), ctx);
}
} catch (err) {
// Error handling: log, keep existing artifacts, continue draining
logger.error(`Rebuild failed: ${request.routePattern}`, err);
}
}
})();
}
}
Manifest Updates During Data Changes
When the slow render server updates instance artifacts, it must also update the route manifest. Since the main server reads the manifest per-request (with timestamp caching), the update must be atomic:
async function updateManifest(
manifestPath: string,
updater: (manifest: RouteManifest) => void,
): Promise<void> {
const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf-8'));
updater(manifest);
// Write to temp file, then atomic rename
const tempPath = manifestPath + '.tmp';
await fs.writeFile(tempPath, JSON.stringify(manifest, null, 2));
await fs.rename(tempPath, manifestPath);
}
Service Initialization
The slow render server needs services to call slowlyRender() (plugins query external APIs). Service initialization follows the same pattern as the main server (DL#134d):
// Same init as main server — plugins sorted topologically, services registered
await initializeProductionServer(buildDir, manifest);
Both servers initialize services independently. Since services are stateless (DL#134 Q14), this is fine — each server gets its own API client instances, database connections, etc.
Renderer HTTP API
POST /_jay/webhooks/:webhookName
Body: (plugin-defined, passed as HttpRequest)
Response: (plugin-defined HttpResponse, plus rebuild stats in headers)
POST /_jay/render/:routePattern
Body: { params?: Record<string, string> }
Response: { rebuilt: number }
Purpose: Manual re-render trigger (CI, admin, debugging)
POST /_jay/render-all
Response: { total: number, rebuilt: number, skipped: number }
Purpose: Force full re-render of all instances
GET /_jay/status
Response: {
version: string,
buildTimestamp: string,
instanceCount: number,
uptime: number,
queueDepth: number,
lastWebhook?: { webhookName: string, timestamp: string }
}
Relationship Between Build and Serve Modes
jay-stack build --version=2
→ Runs Phase 0 + Phase 1 + Phase 2
→ Exits
→ Deploy main server with JAY_BUILD_VERSION=2
jay-stack serve --role=renderer --version=2
→ On startup: validates build/v2/ (rebuilds if stale)
→ Stays alive, listens for webhooks
→ Re-renders affected instances on data change
→ Main server (same version) picks up changes via timestamp caching
Both use the same build pipeline code. The difference:
buildruns once and exitsrendererruns once, then keeps running for incremental updates
Implementation Plan
Step 1: Build Orchestrator
Create buildVersion(version, options):
- Wire Phase 0 → Phase 1 → Phase 2 from DL#134a
- Bounded concurrency for per-instance pipeline
- Progress reporting (instance N/M built)
- Write build-metadata.json
Step 2: CLI Commands
Add to stack-cli:
jay-stack build --version=N— one-shot buildjay-stack serve --role=renderer --version=N— long-running renderer
Step 3: Startup Validation
Implement validateExistingBuild():
- Source hash computation
- Build metadata comparison
- Decision: rebuild / skip / partial
Step 4: makeWebhook Builder
Create makeWebhook(name, handler) in fullstack-component:
- Similar pattern to
makeJayAction - Handler receives
(req: HttpRequest, invalidate: InvalidateRoute) - Returns branded
JayWebhookconstant - Discovery via export scanning (same as actions)
Step 5: Webhook HTTP Server
Create webhook endpoint routing:
- Route
POST /_jay/webhooks/:webhookName - Load registered webhook handler
- Inject
invalidatefunction - Execute handler, return response
Step 6: Invalidation Engine
Implement invalidate(routePattern, params?):
- With params: rebuild specific instance (slow render → compare → compile if changed)
- Without params: re-run loadParams, rebuild all instances, remove deleted
- Atomic manifest update after each invalidation
Step 7: Incremental Instance Rebuild
Implement buildInstance with optimistic skip:
- Slow render with fresh data
- Compare pre-rendered output with existing
- Skip compilation if unchanged
- Full pipeline if changed
Questions
Q1: Should the renderer authenticate webhook requests?
Yes. Webhooks from external systems should be authenticated (shared secret, HMAC signature, or API key). The renderer should reject unauthenticated webhook requests. Authentication details are plugin-specific — each plugin defines how its webhook is authenticated.
Q2: Should the renderer batch rapid webhook events?
If multiple webhooks arrive in quick succession for different instances of the same route, it's efficient to batch them into a single Vite build (per-route batching from DL#134a). A short debounce window (e.g., 1-2 seconds) could accumulate events before triggering the rebuild.
Q3: Should the renderer notify the main server after rebuilding?
Not required — the main server's timestamp-based caching detects file changes naturally. But an optional notification endpoint on the main server could warm caches proactively, improving latency for the first request after a data change.
Trade-offs
| Decision | Pro | Con |
|---|---|---|
| Optimistic skip (compare output) | Avoids unnecessary compilation when data changes don't affect template structure | Extra comparison step; must read existing file |
| Bounded concurrency for builds | Predictable resource usage; doesn't overwhelm external APIs or CPU | Slower than unbounded parallelism for small builds |
makeWebhook pattern (like actions) |
Consistent with existing patterns; webhook logic in code not config; plugins own their data model | Each plugin must implement webhook handler; no default behavior |
Contract-based invalidate API |
Plugin only knows its own contracts; framework resolves to routes; project can remap routes freely | Route manifest must track which contracts each route uses |
| Atomic manifest updates | Main server never reads a partial manifest | Extra write (temp file + rename) |
| Source hash for startup validation | Fast rebuild skip when code unchanged; reliable staleness detection | Hash computation adds startup time; must include all relevant source files |
Verification Criteria
jay-stack buildproduces a complete versioned bucketjay-stack serve --role=rendererstarts and validates existing build- Same-version restart skips rebuild
- Webhook triggers re-render of affected instances only
- Optimistic skip avoids unnecessary compilation
- New instance creation adds to manifest correctly
- Instance deletion removes from manifest correctly
- Manifest updates are atomic (no partial reads)
- Build concurrency stays within configured bounds
- Renderer health endpoint reports accurate status
Implementation Results
Implemented (May 18, 2026)
makeWebhook builder (full-stack-component/lib/jay-webhook-builder.ts):
- Builder pattern matching
makeJayAction:makeWebhook(name).withServices(...).withHandler(fn) - Handler receives
(event: WebhookEvent, invalidate: InvalidateContract, ...services) isJayWebhooktype guard for discovery- 5 tests passing
Contracts field population (builder/instance-pipeline.ts):
RouteEntry.contractspopulated during build from headless component contract names- Collected from
pageParts.headlessInstanceComponentsandpageParts.parts[].contractInfo - 5 tests verifying contract population and contract→route resolution
Rebuild engine (invalidation/rebuild.ts):
rebuild(options)— unified rebuild with three target modes:{ mode: 'contract', contractName, params? }— rebuild all routes using a contract{ mode: 'route', routePattern, params? }— rebuild instances of a specific route{ mode: 'url', url }— resolve URL to route+params via route matcher, rebuild that instance
resolveContractToRoutes(manifest, contractName)— finds routes by contractrebuildContract(options)— convenience wrapper for contract mode (used by webhook handlers)- Optimistic skip: compares pre-rendered output, skips compilation if unchanged
- Atomic manifest update via temp file + rename
Rebuild CLI (stack-cli/lib/run-production.ts):
- Three modes via flags:
jay-stack rebuild --contract product-page --params '{"slug":"x"}'jay-stack rebuild --route /products/[slug](all instances)jay-stack rebuild --url /products/blue-widget(single instance by URL)
--routewithout--paramsrebuilds all instances of the route (useful for pages withpage.tsand no contract)--urlresolves to route+params using the same matcher as the main server
Renderer server (renderer/renderer-server.ts):
jay-stack serve --role=rendererstarts HTTP server with:POST /_jay/webhooks/:name— dispatches to discovered webhook handlersPOST /_jay/rebuild— accepts{ contract, route, url, params }(any one target mode)GET /_jay/status— reports version, uptime, webhook list, last webhook
- Webhook discovery from
plugin.yamldeclarations (matching actions pattern) - Service initialization via shared
init-services.ts
Version derivation (stack-cli/lib/cli.ts):
- Default version derived from project
package.json(major10000 + minor100 + patch) --versionflag overrides for all commands (build, serve, rebuild)
CLI refactor (stack-cli/lib/):
- All CLI commands extracted to dedicated files:
run-dev.ts,run-production.ts,run-validate.ts,run-agent-kit.ts cli.tsis now command registration only (~170 lines)- All commands use
-p, --pathoption instead of positional[path] - Shared
resolveProductionContextfor build/serve/rebuild
Test Coverage
- 85 production-server tests (74 existing + 11 new)
- 5 webhook builder tests
- Full monorepo build: 70 packages pass
Deviations from Design
InvalidationContext/InvalidationQueuefrom the design were not needed —rebuild()handles everything directly. The queue can be added later if batch webhooks need it.- Webhook handler signature uses
WebhookEvent(type, payload, headers) instead of rawHttpRequest— cleaner API, framework handles HTTP parsing - Webhook discovery uses
plugin.yamldeclarations instead of export scanning — consistent with actions - Added
--routeand--urlrebuild modes beyond the original contract-only design — enables rebuilding routes withpage.tsbut no headless contracts - Startup validation (source hash comparison) not implemented yet — listed as remaining work
Atomic Rebuild Transition
Rebuilds produce new files with unique names instead of overwriting existing ones. This prevents a race condition where the main server could serve mismatched files (new pre-rendered HTML with old server element, or vice versa).
Mechanism: InstanceBuildContext.rebuildSuffix (a timestamp-based string) is appended to the params hash input during rebuild, producing a new page_{newHash} instance ID. The initial build has no suffix — same behavior as before.
Sequence:
buildInstancewrites new files:page_{newHash}.jay-html,.cache.json,.server-element.js,-{viteHash}.jsroute-manifest.jsonupdated atomically (temp file + rename) with new file pathsbuild-metadata.jsonupdated with new timestamp — triggers main server to reload manifest- Main server reloads manifest → atomically switches to new files
- Old file paths appended to
cleanup-manifest.json jay-stack cleanup(or async background task) deletes orphaned files
Main server reload trigger: FilesystemArtifactStore.readManifest() checks build-metadata.json mtime. If changed, re-reads the manifest. This is cheaper than checking the manifest itself on every request and ensures the reload only happens after both manifest and metadata are fully written.
Cleanup manifest: Each rebuild appends replaced file paths (pre-rendered HTML, cache JSON, server element, client bundle, CSS) to build/v{n}/cleanup-manifest.json. The jay-stack cleanup command reads this file, deletes the listed files, then removes the manifest itself. Cleanup is safe to run at any time — the main server only reads files referenced by the current route manifest, not orphaned ones.
Known Issues / Future Optimization
- Optimistic skip removed. The original design compared pre-rendered HTML after
buildInstanceto skip unchanged instances. Two problems were found: (1)stripCacheMetadatastripped theslowViewStatefrom the comparison, so data-only changes (e.g., price update that doesn't change template structure) were incorrectly reported as unchanged; (2) the comparison ran after the full pipeline completed, so no work was actually saved. The skip was removed entirely. A proper implementation would compare slowViewState + template content before server element compilation and Vite build — requires splittingbuildInstanceor adding an early-exit path after slow render.
Bug Fixes During Testing
- Silent error swallowing in
getPageParts—page-handler.tshad atry/catchfallback aroundloadPagePartsFromConfigthat silently caught errors and constructed a wrong parts object, causingfastRendercrashes downstream. Removed the catch — errors now propagate with stack traces. - Empty
modulePathinpage-parts.json—buildPagePartsConfigwrote a page entry with emptymodulePathfor pages withoutpage.ts(no server code). Guard changed from fragileparts[0].compDefinitioncheck topageServerModuletruthiness. compDefinitionundefined inrenderFastChangingData— added optional chaining (compDefinition?.fastRender) to guard against parts with no component definition.- Stack traces not printed — server error handler only logged
err.message. Addederr.stacklogging to both main server and renderer server. - Query params lost in production —
page-handler.tsconstructed a new URL frommatch.pathname(path only), losing query string. Now receives the full request URL from the main server. - Local plugin init directory import —
initializeServices(shared) was importingpluginInit.packageNamefor local plugins (a directory path). Fixed to load from compiledserver/plugins/directory, matching the main server's existing fix.
Log Methodology Note
Note: These design logs are written primarily for AI agents as part of the Design Log methodology and made accessible here for human readers. The language and structure are optimized for machine consumption — expect precise, specification-style prose rather than narrative documentation.