[Deep Dive #22] Middleware Pacts - discussion
We have a growing number of cross-cutting behaviors (auth checks, analytics, feature gating) and it's starting to feel like everything is handled in a different place.
Do you centralize cross-cutting logic in a middleware layer, or keep it in route shells and routines? How do you keep middleware behavior observable (signals) so it's not just invisible side effects? What makes a middleware pact stable enough to be treated as contract, not convenience?
Comments (10)
Back to latestIf middleware isn't observable, it's going to become a place where bugs hide. I'm skeptical by default.
We like middleware as a pact only when it's explicit and has signals.
If it changes navigation posture, it must write a signal. If it changes fetch posture, it must write a signal.
Related: Security and Safety and Navigation Routines (Deep Dive #4).
We keep middleware boring: it reads route intent, writes posture, and writes one signal.
ts
function authMiddleware(shell: any) {
const user = shell.read('user');
const ok = Boolean(user);
shell.write('authLane', ok ? 'user' : 'anon');
shell.write('guardSignal', { kind: ok ? 'ok' : 'auth', action: ok ? 'allow' : 'redirect' });
}Counterpoint: middleware can become a hidden dependency if features start relying on it without knowing.
We require every route to declare which pacts apply. No implicit global middleware.
That's a good rule. Implicit global behavior is exactly what we want to avoid.
Declaring pacts per route also makes it clearer when a pact change is a contract change.
Yep. Ownership plus visibility.
We used to put analytics everywhere. Moving it into a pact helped, but only once we made it idempotent and wrote a signal for sent vs skipped.
If you can't test the pact with route-flow tests, it will drift. Signals make it cheap to test without importing internals.
tsx
const route = useRouteState({ pacts: ['auth', 'analytics'] as const });
const doc = useRouteDoc('orders');
return <div data-pacts={route.read('pacts').join(',')} data-auth-lane={doc.read('authLane')} />;We had a bug where a pact changed cache posture and suddenly a screen started refetching constantly.
Once cache lanes were visible as signals, the regression was obvious immediately.
The best pact is one you can delete. Keep it small and typed.
Pacts are basically APIs. If you don't version them and render signals, you'll break routes in subtle ways.
Middleware isn't bad. Invisible middleware is bad.