Observability
Structured logs, request ids on every response, a reporting seam, and a health endpoint.
A launch-quickly app is observable from its first deploy: every log line is structured and filterable, every response carries a request id, alert-worthy errors flow through one seam, and a public health endpoint tells your monitor the truth.
Structured logging, enforced
console.* is a lint error outside src/lib/log.ts. The logger emits one
JSON object per line in production — level, time, msg, plus your fields
— which is the shape platforms like Vercel parse into filterable columns. In
development it prints compact human lines instead.
import { log, requestLog } from '@/lib/log';
const logger = await requestLog({ route: 'billing' });
logger.warn('slow stripe call', { ms: 1200 });Message first, facts as fields. user signed in with { userId } is
filterable; `user ${id} signed in` is prose you will grep for at 2am
and not find.
The logger is dependency-free on purpose: on serverless, stdout JSON is what the platform ingests, and eighty owned lines beat a logging framework's config surface for a starting product. The output shape is pinned by a test — dashboards filter on those keys.
Request ids, end to end
The proxy mints an x-request-id for every request — honoring one supplied
by a trusted upstream — and sets it on the response. requestLog() binds the
same id to every server-side line. So when a user reports a bug, the id in
their network tab is the filter that shows you their request's whole story.
Jobs get the same treatment: handlers receive a log bound with the job id
and the Inngest run id, so a retried run's lines group together.
Error reporting is a seam, not a vendor
reportError(error, fields) is the one function every alert-worthy site
calls — webhook failures, audit-write failures, rate-limiter outages. Wire
Sentry (or any reporter) inside it once and every site is upgraded. Until
then it emits structured error lines with the stack, which every platform can
alert on.
The distinction it encodes: logging is for operators reading a stream; reporting is for waking a human. Expected failures — bad input, missing permission, a spent AI budget — are outcomes, not incidents, and never reach the seam.
The health endpoint
GET /api/health is public and answers two things: whether the process can
reach its database (a real round-trip through the request path's own pool),
and which rate-limit backend is live. Deliberately nothing else — a health
endpoint is also reconnaissance surface. It returns 503 when the database is
unreachable, which is what makes it worth pointing an uptime monitor at.