Basilic
Architecture

Logging

Ops logging is Pino (API/Fastify, Next server) and a console wrapper (browser). Join key is reqId. Product analytics is a separate no-op capture(), not logs.

Server (Next, scripts, pre-listen):

import { logger } from '@repo/utils/logger/server'

logger.info('message')
logger.info({ userId: '123' }, 'User logged in')

const reqId = 'abc'
const err = new Error('timeout')
logger.error({ err, reqId }, 'Request failed')

const reqLogger = logger.child({ reqId })
reqLogger.debug('Processing request')

Browser ('use client'):

import { logger } from '@repo/utils/logger/client'

logger.info('message')
logger.info({ userId: '123' }, 'User logged in')

const reqId = 'abc'
const err = new Error('timeout')
logger.error({ err, reqId }, 'Request failed')

const reqLogger = logger.child({ reqId })
reqLogger.debug('Processing request')

Two processes, one policy:

  • API HTTP: Fastify owns the Pino root and request children (request.log / fastify.log) via createPinoOptions() from @repo/utils/logger/pino-options. Access logs include reqId, method, path (no query), status, latency.
  • Next / scripts: @repo/utils/logger/server singleton (same options factory, JSON in production). Browser: /logger/client.

Never console.* in app UI code. Fastify routes must not emit started/completed/status/latency/path/reqId — that is the access log. Domain events and captureError are extra lines on the same request.

Join key (reqId)

Incoming x-request-id is accepted only if it matches ^[A-Za-z0-9._-]{1,128}$; otherwise Fastify generates a UUID. The field name in logs is reqId, never requestId. Every API response echoes x-request-id. Next BFF/proxy calls forward the same header so web and API lines can join. They are two HTTP hops unless forwarded.

Env

Truthiness uses parseBool: 1, true, yes, on (case-insensitive). Do not use Zod z.coerce.boolean() for these flags ("false" would become true).

RuntimeVariables
API / NodeLOG_ENABLED (default on), LOG_LEVEL (debug | info | warn | error | silent, default info), LOG_SERVICE (default api)
BrowserNEXT_PUBLIC_LOG_ENABLED (default off in production), NEXT_PUBLIC_LOG_LEVEL

In CI, NODE_ENV=test, or VITEST, the default level is silent unless LOG_LEVEL is set. Browser error still emits in production when other levels are off, unless NEXT_PUBLIC_LOG_LEVEL=silent or test/CI.

Config lives in each app lib/env.ts. Logger implementations also read process.env at load.

Redaction

Sanitize sensitive keys (password, token, secret, email, prompt, …) at any depth. Pino also redacts header paths. Log path-only URLs (pathOnlyUrl). Do not log request bodies, AI prompt/messages content, recipients, cookies, or authorization codes. Catalog code is a dedicated field — do not put raw tokens in data.code.

Operational signals (not product analytics)

Auth/email lines (low cardinality): session_issued, auth_verify_failed, auth_locked, email_skipped, email_send_failed, auth_callback_failed, auth_proxy_refresh_failed, auth_proxy_cookie_failed. No success-send or product auth_succeeded log lines — product auth events are no-op capture() calls, not Pino.

Product analytics is not logging: Analytics. Failures: Error Handling.

On this page