Skip to main content

Logging Context System

CONA uses a separate logging context system to enrich logs with organization and user metadata. This is intentionally kept separate from the RLS (Row-Level Security) system to avoid circular dependencies and maintain clean separation of concerns.

Architecture

┌─────────────────────────────────────────────────────────────┐
│                    Request Flow                             │
└─────────────────────────────────────────────────────────────┘

                     │ Auth0 Session Retrieved

┌─────────────────────────────────────────────────────────────┐
│  getAuth0Session()                                          │
│  • Validates session                                        │
│  • Initializes logging context with user.sub               │
└────────────────────┬────────────────────────────────────────┘

                     │ User Fetched from DB

┌─────────────────────────────────────────────────────────────┐
│  getAuth0User()                                             │
│  • Fetches user from database                               │
│  • Updates logging context with userId                      │
└────────────────────┬────────────────────────────────────────┘

                     │ Organization Context Added

┌─────────────────────────────────────────────────────────────┐
│  requireAuth() / getOrganizationId()                        │
│  • Fetches organization from database                       │
│  • Updates logging context with:                            │
│    - organizationId                                         │
│    - organizationSlug                                       │
└────────────────────┬────────────────────────────────────────┘

                     │ All Subsequent Logs Include Context

┌─────────────────────────────────────────────────────────────┐
│  Axiom Logs                                                 │
│  • organizationId                                           │
│  • organizationSlug                                         │
│  • userId                                                   │
│  • userSub                                                  │
└─────────────────────────────────────────────────────────────┘

Why Separate from RLS?

RLS System (packages/database/src/rls.ts)

  • Purpose: Database security and multi-tenant isolation
  • Scope: PostgreSQL session configuration variables
  • Data: orgId, userId, sub (Auth0 ID)
  • Used by: PostgreSQL RLS policies

Logging Context (apps/webapp/lib/logging-context.ts)

  • Purpose: Enrich logs with metadata for monitoring and debugging
  • Scope: Request-scoped context (isolated via AsyncLocalStorage)
  • Data: organizationId, organizationSlug, userId, userSub
  • Used by: Axiom logging, middleware logging, security monitoring

Benefits of Separation

  1. No Circular Dependencies: Logging doesn’t depend on database queries
  2. Clean Separation of Concerns: Security (RLS) vs Observability (Logging)
  3. Independent Evolution: Can change logging strategy without affecting RLS
  4. Performance: Logging context is lightweight and doesn’t touch the database

API Reference

initLoggingContext(params)

Initialize logging context for a new request (typically called once per request).
import { initLoggingContext } from "@/lib/logging-context";

initLoggingContext({
  userSub: "auth0|123456",
  userId: "user-uuid",
  organizationId: "org-uuid",
  organizationSlug: "acme-corp",
});

updateLoggingContext(params)

Update specific fields in the logging context (called as more data becomes available).
import { updateLoggingContext } from "@/lib/logging-context";

// After fetching user from database
updateLoggingContext({
  userId: user.id,
});

// After fetching organization
updateLoggingContext({
  organizationId: org.id,
  organizationSlug: org.slug,
});

getLoggingContext()

Get the current logging context (read-only).
import { getLoggingContext } from "@/lib/logging-context";

const context = getLoggingContext();
console.log(context.organizationSlug); // "acme-corp"

Usage Examples

Middleware Logging

import { logMiddlewareRequest } from "@/lib/middleware-logger";

export async function middleware(request: NextRequest) {
  // Automatically includes logging context if available
  logMiddlewareRequest(request);
}
Logged Fields:
  • organizationSlug (if auth’d)
  • organizationId (if auth’d)
  • userId (if auth’d)
  • Request URL, method, IP, etc.

Security Event Logging

import { logSecurityEvent } from "@/lib/middleware-logger";

logSecurityEvent(request, response, {
  action: "auth_required",
  reason: "No valid session",
  blocked: true,
});
Logged Fields:
  • All fields from logging context
  • Security-specific metadata
  • source: "security" tag

Server Action Logging

import { log } from "@/app/lib/logger";
import { getLoggingContext } from "@/lib/logging-context";

export async function serverAction() {
  const context = getLoggingContext();
  
  await log({
    message: "Processing invoice",
    level: "info",
    metadata: {
      ...context, // Include all logging context
      invoiceId: "inv-123",
    },
  });
}

Where Context is Set

1. Authentication Flow (auth.ts)

getAuth0Session() - Sets userSub
initLoggingContext({
  userSub: session.user.sub,
});
getAuth0User() - Adds userId
updateLoggingContext({
  userId: user.id,
});
requireAuth() - Adds organizationId and organizationSlug
updateLoggingContext({
  organizationId,
  organizationSlug: organization.slug,
});

Best Practices

  1. Initialize Early: Call initLoggingContext() as soon as you have the Auth0 session
  2. Update Incrementally: Use updateLoggingContext() as more data becomes available
  3. Read-Only Access: Always use getLoggingContext() to access context (don’t modify directly)
  4. Request Isolation: Context is automatically isolated per-request using AsyncLocalStorage

Security Considerations

Request Isolation with AsyncLocalStorage

The logging context uses Node.js’s AsyncLocalStorage to ensure each request has its own isolated context. This prevents race conditions where concurrent requests could overwrite each other’s context. Without AsyncLocalStorage (vulnerable):
// ❌ Module-level variable - shared across all requests
let loggingContext = {};

// Request A: initLoggingContext({ userId: "user-a" })
// Request B: initLoggingContext({ userId: "user-b" }) 
// Request A logs → incorrectly tagged with user-b!
With AsyncLocalStorage (secure):
// ✅ Request-scoped storage - isolated per request
const asyncLocalStorage = new AsyncLocalStorage<LoggingContext>();

// Request A and B have separate, isolated contexts
// No race conditions or cross-contamination

Why This Matters for Security Auditing

Security events must be accurately attributed to the correct user and organization. Without proper request isolation:
  • Brute force detection could flag the wrong IP/user
  • Unauthorized access logs could show incorrect user IDs
  • Audit trails would be unreliable for compliance
  • Security incidents couldn’t be accurately investigated
AsyncLocalStorage ensures security logs are always attributed to the correct request context.

Comparison: RLS vs Logging Context

AspectRLS SystemLogging Context
PurposeDatabase securityLog enrichment
StoragePostgreSQL session varsAsyncLocalStorage (request-scoped)
DataorgId, userId, suborgId, orgSlug, userId, userSub
Updated byinitRlsWrapper()initLoggingContext()
Used byPostgreSQL RLS policiesAxiom logs, middleware
ScopeDatabase transactionsPer-request (isolated)

Migration Notes

Prior to this system, organization slug was stored in RLS state, causing circular dependencies between logging and database layers. The new system cleanly separates these concerns: Before:
// ❌ Circular dependency
import { getRlsState } from "@/app/lib/db/rls";
const rlsState = getRlsState();
logger.info("Event", { orgSlug: rlsState.orgSlug });
After:
// ✅ Clean separation
import { getLoggingContext } from "@/lib/logging-context";
const context = getLoggingContext();
logger.info("Event", { orgSlug: context.organizationSlug });