Devtools
@contract-kit/devtools gives local apps a live timeline for Contract Kit activity: requests, errors, use cases, domain events, jobs, and provider activity.
bun add @contract-kit/devtools
Setup
1. Register the provider and hook
import { createDevtoolsHooks, devtoolsProvider } from "@contract-kit/devtools";
import { createNextServer } from "@contract-kit/next";
export const server = await createNextServer({
ports,
providers: [devtoolsProvider, ...otherProviders],
hooks: [createDevtoolsHooks()],
createContext: async ({ req, ports }) => ({
requestId: req.headers.get("x-request-id") ?? crypto.randomUUID(),
ports,
}),
});
createDevtoolsHooks() records HTTP request timing and errors. It skips /api/devtools by default so dashboard traffic does not pollute the timeline. When a request does not already have an ID, the hook generates one and exposes it with the x-request-id response header.
2. Add the dashboard route
// app/api/devtools/[[...path]]/route.ts
import { handleDevtoolsRequest } from "@contract-kit/devtools";
import { server } from "@/server";
export async function GET(req: Request) {
return handleDevtoolsRequest(req, server.ports.devtools, "/api/devtools");
}
export async function POST(req: Request) {
return handleDevtoolsRequest(req, server.ports.devtools, "/api/devtools");
}
Visit /api/devtools in development.
The dashboard uses Server-Sent Events for live updates and falls back to polling when needed. It includes tabs for the full timeline, requests, use cases, errors, domain events, jobs, and providers. Request rows expand to show events with the same requestId.
3. Instrument use cases
Use a shared application factory:
import { createUseCaseFactory } from "@contract-kit/application";
import { createDevtoolsUseCaseObserver } from "@contract-kit/devtools";
export const useCase = createUseCaseFactory<AppContext>({
onRun: createDevtoolsUseCaseObserver<AppContext>(),
});
The observer reads ctx.ports.devtools and ctx.requestId, so use case events are correlated with the request that triggered them.
Manual events
Use record() when application code wants to add a custom event. It fills id and timestamp for you.
ctx.ports.devtools.record({
type: "job",
jobName: "posts.reindex",
status: "scheduled",
});
Use log() only when you already have a complete DevtoolsEvent.
Redaction
Devtools applies default redaction before events are stored. Sensitive keys such as authorization, cookie, set-cookie, x-api-key, token, password, and secret are replaced with [redacted].
Request hooks record request headers for debugging, but they do not record request or response bodies by default.
createDevtoolsHooks({
redact: (event) => ({
...event,
details: scrub(event.details),
}),
});
Event types
| Type | Description | Key fields |
|---|---|---|
request | HTTP request handling | method, path, status, durationMs |
error | Errors | message, stack, contractName, useCaseName |
usecase | Use case execution | name, kind, phase, durationMs |
eventBus | Domain event publishing | eventName |
job | Background job lifecycle | jobName, status |
provider | Provider lifecycle | providerName, action |
All events share id, timestamp, and an optional requestId.
Endpoints
| Endpoint | Description |
|---|---|
GET /api/devtools | Dashboard UI |
GET /api/devtools/events | JSON event list |
GET /api/devtools/stream | Server-Sent Events stream |
POST /api/devtools/clear | Clear the in-memory buffer |
Configuration
Devtools is enabled when NODE_ENV !== "production" and disabled in production.
DEVTOOLS_ENABLED=true
DEVTOOLS_ENABLED=false
DEVTOOLS_MAX_EVENTS=1000
The default in-memory buffer keeps the latest 500 events. The events endpoint returns the latest 200 unless a limit query parameter is provided.