Skip to main content

Architecture & Integration Patterns

@simplelogs/next supports two integration patterns: the Provider pattern and the Standalone pattern. Understanding the tradeoffs helps you choose the right approach for your project.

How the SDK works

The SDK maintains a separate internal queue for server-side and client-side log entries. Each queue batches entries and flushes them to the SimpleLogs ingestion endpoint on an interval, when the batch reaches its max size, or when the page unloads.

graph TD
A[Server Component<br/>serverLogger.log] --> B[Server Queue]
C[Client Component<br/>useSimpleLogs / clientLogger] --> D[Client Queue]
B -- flush --> E[POST /api/v1/enqueue<br/>source: server]
D -- flush / beacon --> F[POST /api/v1/enqueue<br/>source: client]
E --> G[SimpleLogs]
F --> G

The server queue uses fetch with await. The client queue uses navigator.sendBeacon by default (fire-and-forget), falling back to fetch with keepalive: true.

Integration patterns

Place <SimpleLogsProvider> in your root app/layout.tsx. It:

  1. Calls configureSDK() during React Server Component rendering (configures the server-side module)
  2. Wraps children in a React Context that delivers the client config to every client component in the tree
app/layout.tsx
import { SimpleLogsProvider } from '@simplelogs/next';

export default function RootLayout({ children }) {
return (
<SimpleLogsProvider
config={{
serverKey: process.env.SIMPLELOGS_SERVER_KEY!,
clientKey: process.env.SIMPLELOGS_CLIENT_KEY!,
}}
>
{children}
</SimpleLogsProvider>
);
}

Client components access the logger via useSimpleLogs() — no prop drilling required.

Standalone pattern (no provider)

If you don't want to add a provider to your layout, you can configure the SDK imperatively and use the module-level loggers directly.

Server side:

lib/logger.ts
import { serverLogger, configureSDK } from '@simplelogs/next/server';

// Call once at module load time — or at the top of each server action/route
configureSDK({ serverKey: process.env.SIMPLELOGS_SERVER_KEY! });

export { serverLogger };

Client side:

lib/client-logger.ts
import { SimpleLogs } from '@simplelogs/next';

// Call once, e.g. in a top-level client component's useEffect
SimpleLogs.init({ clientKey: process.env.NEXT_PUBLIC_SIMPLELOGS_CLIENT_KEY! });

export { SimpleLogs };

Choosing an integration pattern

ConsiderationProviderStandalone
Setup complexityOne provider in layout — minimalCall init() / configureSDK() manually
Client config propagationAutomatic via React ContextManual — call SimpleLogs.init() once before logging
Server key exposure riskProvider runs as a Server Component — key stays server-sideSame, as long as you only import server exports from server code
Works without ReactNo — requires React Context for client sideYes — can use module-level loggers directly
Deferred initSupported via clientInit={false} + <SimpleLogsClientInit />Natural — just call init() whenever you're ready
Suspense boundariesUse clientInit={false} to defer past a Suspense boundaryNo concern
Multiple workspaces / keysOne provider per key set (unusual)Call configureSDK() with different config where needed

Use the Provider unless:

  • You're adding SimpleLogs to a non-Next.js React app
  • You need fine-grained control over exactly when the client is initialized (e.g., after a Suspense boundary resolves)
  • You want to use the SDK from a non-React context (plain Node scripts, API routes without React)

Use Standalone when you need explicit control or are not using React's component tree for initialization.

Deferred client initialization

By default, <SimpleLogsProvider clientInit={true}> applies client config as soon as the provider mounts. Set clientInit={false} to delay initialization, then place <SimpleLogsClientInit /> wherever in the tree you want config to be applied:

app/layout.tsx
<SimpleLogsProvider config={...} clientInit={false}>
<Suspense fallback={<Loading />}>
<SimpleLogsClientInit /> {/* applied after Suspense resolves */}
{children}
</Suspense>
</SimpleLogsProvider>

useSimpleLogs() is safe to call anywhere under the provider even when clientInit={false} — logging calls before SimpleLogsClientInit mounts are queued and flushed once config is applied.

Serverless environments

On Vercel (and other serverless/edge runtimes), the SDK detects process.env.VERCEL and automatically switches the queue to immediate flush mode (serverless: true). In this mode every log() / start() / end() call triggers an immediate fetch to the ingestion endpoint rather than batching.

This is necessary because serverless functions don't persist state between invocations — a queued batch would be lost when the function finishes.

You can force this behavior explicitly:

configureSDK({ serverless: true });

Fire-and-forget vs. awaited sends

The client-side queue defaults to fireAndForget: true, which uses navigator.sendBeacon. Beacon calls:

  • Do not block the page from unloading
  • Are not retried on failure
  • Do not return a response body

If you set fireAndForget: false, the client queue uses fetch instead. Failed sends are re-queued and retried on the next flush. Use this when delivery reliability matters more than performance.

configureSDK({ fireAndForget: false });

The server-side queue always uses fetch and awaits the response.

Config resolution order

Config values are resolved in this priority order (highest wins):

  1. SIMPLELOGS_* environment variables
  2. Values passed to configureSDK() / <SimpleLogsProvider config={...}>
  3. SDK defaults