Adds a per-subsystem measurement of reactive.js, made by minifying it repeatedly with one subsystem removed rather than counting source bytes. This corrects the earlier audit on both figures and on the conclusion drawn from them. Component controllers are 23,722 bytes minified / 6,660 gzipped -- 30.6% of transfer, not the "about 18%" previously claimed -- and splitting them out saves 6.6 kB gzipped on a typical page, not "3-4 kB". Measured against the example app, / and /login use none of the ten controllers and /layout uses one, so most pages download and parse the lot for nothing. The larger finding is that the runtime is not where the weight is. One page parses 490,212 decoded bytes across 11 generated client modules while transferring 21,026, and the largest module is 89.8% duplicated lines: the state-restore prologue appears 162 times because client-codegen.ts inlines the sync into every peer alias of every client function. Gzip hides it on the wire, but parse cost follows decoded bytes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@wrnexus/rpc
Define a service contract in a shared workspace package, then import that same contract from the caller and callee.
import {
defineService,
implement,
inProcessTransport,
procedure,
serviceClient,
} from "@wrnexus/rpc";
import { v } from "@wrnexus/validation";
const greeter = defineService({
name: "greeter",
procedures: {
greet: procedure
.input(v.object({ name: v.string() }))
.output<{ message: string }>()
.build(),
},
});
const service = implement(
greeter,
{ greet: async ({ name }) => ({ message: `Hello, ${name}` }) },
{ selfApp: "greeter" },
);
const client = serviceClient(greeter, {
app: "greeter",
transport: inProcessTransport({
"greeter/greet": (input, identity) => service.invoke("greet", input, identity),
}),
});
await client.greet({ name: "Ada" });
Service files default-export implement(...) from app/services. The development server mounts them under the private /__wrnexus/rpc prefix.
Pass { as: ctx } to serviceClient to propagate the subject. The signed token contains only subject and tenant identifiers; permissions are always checked by the callee. Set WRNEXUS_RPC_SECRET in every app, use at least 32 characters, and never reuse the session secret.
Calls time out by default. Retrying is intentionally deferred; when introduced, only procedures marked .idempotent() may be retried.
Deployment requirement: apps must be unreachable except through the gateway
/__wrnexus/rpc/* is authenticated by TWO signals together: a marker header
(x-wrnexus-internal: 1) AND the absence of any X-Forwarded-* header. The
WrNexus gateway satisfies this by construction — it strips any inbound
marker header from the public request, and it always adds X-Forwarded-*
when proxying to an app. A direct loopback call from a sibling app process
carries the marker and no forwarded headers, so it passes; anything that
came through the gateway carries forwarded headers, so it's rejected even if
it also carries the marker.
This check only works if the app process is unreachable except through the
gateway. If an app's port is exposed directly, or if a reverse proxy sits
in front of it WITHOUT setting X-Forwarded-* (a bare proxy_pass with no
proxy_set_header X-Forwarded-For/X-Forwarded-Host/X-Forwarded-Proto),
then an external caller can set the marker header itself, arrive with no
forwarded headers, and reach /__wrnexus/rpc/* as if it were an internal
call — bypassing the gateway's edge block entirely.
Requirements for any deployment:
- App processes must bind to a private/loopback interface and be reachable ONLY through the gateway (or an equivalent trusted front door) — never exposed directly to the internet or an untrusted network.
- Any reverse proxy placed in front of an app (nginx, a load balancer, etc.)
MUST set
X-Forwarded-For,X-Forwarded-Host, andX-Forwarded-Protoon every request it forwards. Omitting these silently reopens the private RPC namespace to anyone who can reach the proxy.