Factory that creates the Fastify middleware and probe endpoint.
import Fastify from 'fastify';
import cookie from '@fastify/cookie';
import { createDeviceRouter } from '@device-router/middleware-fastify';
import { MemoryStorageAdapter } from '@device-router/storage';
const app = Fastify();
await app.register(cookie);
const { middleware, probeEndpoint } = createDeviceRouter({
storage: new MemoryStorageAdapter(),
});
app.post('/device-router/probe', probeEndpoint);
app.addHook('preHandler', middleware);| Option | Type | Default | Description |
|---|---|---|---|
storage |
StorageAdapter |
required | Storage backend |
cookieName |
string |
'device-router-session' |
Session cookie name |
cookiePath |
string |
'/' |
Cookie path |
cookieSecure |
boolean |
false |
Set Secure flag on the session cookie |
ttl |
number |
86400 |
Profile TTL in seconds |
rejectBots |
boolean |
true |
Reject bot/crawler probe submissions (returns 403) |
thresholds |
TierThresholds |
built-in defaults | Custom tier classification thresholds (validated at startup) |
injectProbe |
boolean |
false |
Auto-inject probe script into HTML responses |
probePath |
string |
'/device-router/probe' |
Custom probe endpoint path for injected script |
probeNonce |
string | ((req: FastifyRequest) => string) |
— | CSP nonce for the injected script tag |
fallbackProfile |
FallbackProfile |
— | Fallback profile for first requests without probe data |
classifyFromHeaders |
boolean |
false |
Classify from UA/Client Hints on first request |
onEvent |
OnEventCallback |
— | Observability callback for logging/metrics (guide) |
| Property | Type | Description |
|---|---|---|
middleware |
preHandler hook |
Classifies the device and attaches profile to req |
probeEndpoint |
Fastify route handler | Handles POST from probe, validates and stores signals |
injectionMiddleware |
onSend hook or undefined |
Only present when injectProbe: true |
The middleware attaches a ClassifiedProfile | null to req.deviceProfile:
interface ClassifiedProfile {
profile: DeviceProfile; // Raw profile with signals
tiers: DeviceTiers; // { cpu, memory, connection, gpu }
hints: RenderingHints; // { deferHeavyComponents, ... }
source: ProfileSource; // 'probe' | 'headers' | 'fallback'
}null when no session cookie is present or profile has expired (unless classifyFromHeaders or fallbackProfile is configured).
When injectProbe: true, injectionMiddleware is returned as an onSend hook that injects the probe <script> into HTML responses. Register it yourself. Requires @device-router/probe to be installed.
const { middleware, probeEndpoint, injectionMiddleware } = createDeviceRouter({
storage,
injectProbe: true,
probeNonce: 'my-nonce', // or (req) => req.headers['x-nonce']
});
app.addHook('preHandler', middleware);
if (injectionMiddleware) {
app.addHook('onSend', injectionMiddleware);
}The script is injected before </head>, falling back to </body>. JSON and other non-HTML responses pass through unmodified.
The individual pieces can be used independently for more granular control.
Reads and returns the minified probe script. Use this with createInjectionMiddleware() when you need probe injection without the full factory.
import { loadProbeScript } from '@device-router/middleware-fastify';
const probeScript = loadProbeScript();
// or with a custom endpoint path:
const probeScript = loadProbeScript({ probePath: '/custom/probe' });| Option | Type | Default | Description |
|---|---|---|---|
probePath |
string |
— | Custom probe endpoint path (rewrites the URL in the script) |
Creates the preHandler hook independently. Thresholds are validated at creation time.
import { createMiddleware } from '@device-router/middleware-fastify';
const hook = createMiddleware({
storage,
thresholds: { cpu: { lowUpperBound: 4, midUpperBound: 8 } },
});
app.addHook('preHandler', hook);| Option | Type | Default | Description |
|---|---|---|---|
storage |
StorageAdapter |
(required) | Storage backend for profiles |
cookieName |
string |
'device-router-session' |
Session cookie name |
thresholds |
TierThresholds |
Built-in | Custom tier thresholds (validated at creation) |
fallbackProfile |
FallbackProfile |
— | Fallback profile for first requests |
classifyFromHeaders |
boolean |
false |
Classify from UA/Client Hints on first request |
onEvent |
OnEventCallback |
— | Observability callback |
Creates the probe POST handler independently.
import { createProbeEndpoint } from '@device-router/middleware-fastify';
const endpoint = createProbeEndpoint({
storage,
ttl: 3600,
rejectBots: true,
});
app.post('/device-router/probe', endpoint);| Option | Type | Default | Description |
|---|---|---|---|
storage |
StorageAdapter |
(required) | Storage backend for profiles |
cookieName |
string |
'device-router-session' |
Session cookie name |
cookiePath |
string |
'/' |
Cookie path |
cookieSecure |
boolean |
false |
Set Secure flag on the cookie |
ttl |
number |
86400 |
Profile TTL in seconds |
rejectBots |
boolean |
true |
Reject bot/crawler probe submissions |
onEvent |
OnEventCallback |
— | Observability callback |
Creates the onSend injection hook independently. Pair with loadProbeScript() to load the probe bundle.
import { createInjectionMiddleware, loadProbeScript } from '@device-router/middleware-fastify';
const injection = createInjectionMiddleware({
probeScript: loadProbeScript({ probePath: '/api/probe' }),
nonce: 'my-csp-nonce',
});
app.addHook('onSend', injection);| Option | Type | Default | Description |
|---|---|---|---|
probeScript |
string |
(required) | The minified probe script source |
nonce |
string | ((req: FastifyRequest) => string) |
— | CSP nonce for the injected script |
Use the pieces independently when you need fine-grained control:
import Fastify from 'fastify';
import cookie from '@fastify/cookie';
import {
createMiddleware,
createProbeEndpoint,
createInjectionMiddleware,
loadProbeScript,
} from '@device-router/middleware-fastify';
import { MemoryStorageAdapter } from '@device-router/storage';
const app = Fastify();
const storage = new MemoryStorageAdapter();
await app.register(cookie);
// Each piece is configured independently
const hook = createMiddleware({ storage });
const endpoint = createProbeEndpoint({ storage, ttl: 3600 });
const injection = createInjectionMiddleware({
probeScript: loadProbeScript(),
});
app.addHook('preHandler', hook);
app.addHook('onSend', injection);
app.post('/device-router/probe', endpoint);
app.get('/', (req, reply) => {
const profile = req.deviceProfile;
reply.send({ tiers: profile?.tiers });
});
app.listen({ port: 3000 });@fastify/cookiemust be registered before the DeviceRouter middleware@device-router/probemust be installed when usinginjectProbe: true