ShipKit docs
The stack

Observability

Structured logs with LogTape, errors to Sentry, product events to PostHog. All three optional.

Three separate concerns, three separate tools, and none of them required to run the app. A fresh clone logs to the console and sends nothing anywhere.

ConcernToolOff by default
Structured logsLogTapeAlways on, console sink
Error captureSentryYes, until SENTRY_DSN is set
Product analyticsPostHogYes, until the keys are set

Logs

configureLogging() is idempotent. Every entrypoint calls it first, so no app has to coordinate with another about who initialises logging.

import { getLogger } from "@workspace/logger"

const log = getLogger(["api", "checkout"])

log.info("Created checkout for {userId}", { userId: user.id })

Categories are dotted paths and the placeholder syntax is LogTape's, not a template literal. Writing log.info(`Created checkout for ${user.id}`) works but throws away the structure: the whole point is that userId arrives as a field a log pipeline can filter on, rather than baked into a string.

LOG_LEVEL controls verbosity, defaulting to info in production and debug everywhere else. LogTape's own meta logger is pinned at warning so it stays out of the way.

Errors

Set SENTRY_DSN and the API and worker report; set NEXT_PUBLIC_SENTRY_DSN and the browser bundles do too. SENTRY_TRACES_SAMPLE_RATE controls tracing volume, and SENTRY_RELEASE ties an event to a build.

The worker's error handling is worth reading before you copy it: it reports only when a job has exhausted its retries, not on each attempt. See background jobs for why that guard exists.

Product analytics

NEXT_PUBLIC_POSTHOG_KEY and NEXT_PUBLIC_POSTHOG_HOST turn on the browser client. The server-side key falls back to the public one when unset, which is fine for most projects because PostHog's project key is not a secret.

The API flushes PostHog on SIGTERM before the server stops. Without that, the events from the last few seconds of a deploy are lost, which is exactly the window where a deploy-related error would have shown up.

There is no /health endpoint. The closest thing today is /api/openapi answering, which proves the process is up and its routes registered. If you need a real liveness probe, add one route rather than pointing a monitor at a document.

What is not wired

OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_SERVICE_NAME are read by the environment schema, so the variables validate and can be passed through, but there is no exporter shipped and nothing emits spans. Treat them as a reservation rather than a feature: if you need traces, wire the SDK yourself and the configuration surface is already there.

On this page