@soft-where/meter
v1.0.3
Published
SOFT-WHERE usage metering library — production-safe, edge-compatible, stateless
Maintainers
Readme
@soft-where/meter
Production-safe, edge-compatible usage metering for Node and Next.js. Tracks API requests, bandwidth, blob storage, custom metrics, and DB snapshots by posting to the SOFT-WHERE ERP. Stateless, non-blocking, and only active when NODE_ENV === "production".
Install
npm install @soft-where/meterRequired variables
The meter needs three values. Pass them in when calling createMeter(); in apps you typically read from environment variables so secrets stay out of code.
| Variable | Purpose | Example |
|----------|---------|--------|
| projectId | Your SOFT-WHERE project identifier | "proj_abc123" |
| apiKey | Secret key for the ERP (do not expose to the client) | From SOFT-WHERE dashboard |
| erpUrl | Full URL of the SOFT-WHERE ERP ingest endpoint | "https://erp.soft-where.com/ingest" |
| environment | Optional label (e.g. staging vs production) | "production" |
| development | Optional. When true, logs every meter operation (success, failure, and skipped) to the terminal. | true in dev, omit or false in production |
Important: Do not use NEXT_PUBLIC_* for apiKey or other secrets. Use server/edge-only env vars (e.g. SOFT_WHERE_API_KEY) so they are not bundled for the browser.
Usage
1. Create a meter
Call createMeter() once (e.g. at module load or in a shared helper) and reuse the returned object. Use env vars for config so you can change them per environment without code changes.
import { createMeter } from "@soft-where/meter";
const meter = createMeter({
projectId: process.env.SOFT_WHERE_PROJECT_ID!,
apiKey: process.env.SOFT_WHERE_API_KEY!,
erpUrl: process.env.SOFT_WHERE_ERP_URL!,
environment: process.env.VERCEL_ENV ?? process.env.NODE_ENV,
development: process.env.NODE_ENV !== "production", // verbose logging in dev
});2. Use the meter
- trackRequest(req, res, durationMs, bandwidthBytes?) — Call with the same
RequestandResponse(or Next.jsNextRequest/NextResponse) and the request duration in milliseconds. The library sends one request event. By default it reads bandwidth fromResponse#headersContent-Length; that header is often missing in Next.js middleware (because the response body is produced after middleware runs), so bandwidth is then reported as 0. Pass the optional fourth argumentbandwidthByteswhen you know the response size (e.g. from a route handler or response wrapper) so the ERP gets accurate bandwidth. - trackBlobUpload(bytes) / trackBlobDelete(bytes) — Call from upload/delete handlers with the byte deltas.
- trackCustomMetric(name, value) — Send arbitrary numeric metrics.
- attemptDbSnapshot(getDbSize) — Call with an async function that returns DB size in bytes (e.g. Neon HTTP). The library will call it, then post the snapshot to the ERP without blocking on the network.
All methods are no-ops when NODE_ENV !== "production". They never throw and never block request completion (ERP calls are fire-and-forget with keepalive: true).
Tracking bandwidth: The library reports bandwidth from the response Content-Length header when present. In Next.js middleware the response from NextResponse.next() is created before the body exists, so Content-Length is usually missing and bandwidth is 0. To report accurate bandwidth, pass the response body size as the fourth argument: trackRequest(req, res, durationMs, responseBodyBytes). You can supply this from API routes that know their payload size, or from any layer that measures the outgoing response (e.g. a wrapper that counts bytes).
Development mode (verbose logging)
Pass development: true in createMeter() to enable verbose terminal logging for every meter operation:
- ✓ success — Payload was sent and the ERP responded with success (e.g. 2xx).
- ✗ failure — Send failed (network error, non-2xx response, or exception).
- ○ skipped — Operation was not sent because
NODE_ENV !== "production".
Use this in local or staging to confirm the meter is being called and that the ERP is reachable. Leave development unset or false in production to avoid log noise.
const meter = createMeter({
projectId: process.env.SOFT_WHERE_PROJECT_ID!,
apiKey: process.env.SOFT_WHERE_API_KEY!,
erpUrl: process.env.SOFT_WHERE_ERP_URL!,
development: process.env.NODE_ENV !== "production",
});Next.js: use in middleware.ts
Middleware runs on the Edge runtime. You can use @soft-where/meter there to send request counts and duration to the SOFT-WHERE ERP. Bandwidth: in middleware, the response from NextResponse.next() usually has no Content-Length yet (the body is generated later), so bandwidth is reported as 0 unless you pass it explicitly (see below).
Cursor agent instructions (Next.js middleware integration)
Use the following as both documentation and a prompt for a Cursor agent that should wire this package into a Next.js app’s middleware.
Goal: Integrate @soft-where/meter into the existing Next.js middleware so that every request is metered and reported to the SOFT-WHERE ERP. Do not break existing middleware logic (auth, redirects, etc.); add metering alongside it.
Steps:
Install the package
- Add dependency:
@soft-where/meter.
- Add dependency:
Environment variables
- Ensure the Next.js project has these server/edge-only env vars (e.g. in
.env.localor in the hosting provider’s dashboard). Do not prefix them withNEXT_PUBLIC_:SOFT_WHERE_PROJECT_ID— SOFT-WHERE project ID.SOFT_WHERE_API_KEY— Secret API key for the ERP.SOFT_WHERE_ERP_URL— Full ERP ingest URL (e.g.https://erp.soft-where.com/ingest).
- Document these in the project’s env example (e.g.
.env.example) with short comments.
- Ensure the Next.js project has these server/edge-only env vars (e.g. in
Create a shared meter instance
- In the same file as the middleware (or in a small module it imports), create the meter once using the env vars above:
createMeter({ projectId, apiKey, erpUrl, environment?, development? }).- Read
projectId,apiKey, anderpUrlfromprocess.env.SOFT_WHERE_PROJECT_ID,process.env.SOFT_WHERE_API_KEY, andprocess.env.SOFT_WHERE_ERP_URL. - Optionally set
development: process.env.NODE_ENV !== "production"to get verbose success/failure logging in the terminal during development. - Only create/call the meter when these env vars are defined so the app does not crash in dev or when the integration is disabled.
- In the same file as the middleware (or in a small module it imports), create the meter once using the env vars above:
Wire metering into
middleware.ts- In the middleware function:
- Record the start time at the very beginning (e.g.
const start = Date.now()). - Run the existing middleware logic and obtain the
NextResponse(or other response) you intend to return. - Before returning, call
meter.trackRequest(request, response, durationMs)ormeter.trackRequest(request, response, durationMs, bandwidthBytes)where:requestis the middleware’sNextRequest(it is a standardRequest).responseis theNextResponse(or response) you are about to return (it is a standardResponse).durationMsisDate.now() - start.bandwidthBytes(optional): in middleware, the response often has noContent-Length, so bandwidth defaults to 0. Pass the actual response body size here if you have it (e.g. from a header you set elsewhere or from measuring the response). Otherwise omit it.
- Return the same response as before so behavior is unchanged.
- Record the start time at the very beginning (e.g.
- Do not
awaitthe meter;trackRequestis fire-and-forget and must not block the response.
- In the middleware function:
Edge compatibility
- Middleware runs on the Edge runtime.
@soft-where/meteris edge-compatible and uses the globalfetchwithkeepalive: true. No extra config is required.
- Middleware runs on the Edge runtime.
Production-only behavior
- The library only sends data when
NODE_ENV === "production". In development, all meter calls are no-ops; no need to guard calls in code.
- The library only sends data when
Summary for the agent: Install @soft-where/meter, add env vars SOFT_WHERE_PROJECT_ID, SOFT_WHERE_API_KEY, and SOFT_WHERE_ERP_URL (no NEXT_PUBLIC_), create one meter with createMeter() from those env vars, then in middleware.ts measure duration and call meter.trackRequest(request, response, durationMs) before returning the response. Optionally pass a fourth argument bandwidthBytes if the app can provide response body size (otherwise bandwidth is 0 in middleware). Leave all other middleware logic intact.
Minimal middleware example
// middleware.ts (Next.js)
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { createMeter } from "@soft-where/meter";
const projectId = process.env.SOFT_WHERE_PROJECT_ID;
const apiKey = process.env.SOFT_WHERE_API_KEY;
const erpUrl = process.env.SOFT_WHERE_ERP_URL;
const meter =
projectId && apiKey && erpUrl
? createMeter({ projectId, apiKey, erpUrl })
: null;
export function middleware(request: NextRequest) {
const start = Date.now();
const response = NextResponse.next();
const durationMs = Date.now() - start;
if (meter) {
meter.trackRequest(request, response, durationMs);
}
return response;
}API reference
createMeter(options)
options.projectId(string),options.apiKey(string),options.erpUrl(string),options.environment?(string),options.development?(boolean). Whendevelopmentistrue, all meter operations are logged to the terminal (success, failure, and skipped).
Returns a Meter object.meter.trackRequest(req, res, durationMs, bandwidthBytes?)
reqandresare the Web APIRequestandResponse(or Next.js equivalents).durationMsis the request duration in milliseconds. Sends one request event. Bandwidth is taken from the optionalbandwidthBytesargument if provided and ≥ 0; otherwise fromres.headersContent-Length(often 0 in Next.js middleware). PassbandwidthByteswhen you know the response body size so the ERP gets accurate bandwidth.meter.trackBlobUpload(bytes) / meter.trackBlobDelete(bytes)
bytesis the size delta.meter.trackCustomMetric(name, value)
name(string),value(number).meter.attemptDbSnapshot(getDbSize)
getDbSizeis a() => Promise<number>(e.g. Neon HTTP size). The library calls it and posts the result to the ERP without awaiting the network. Returns a Promise that resolves when the size has been obtained and the send has been triggered (or on error); never throws.
License
MIT
