webhooks.lol is a pnpm workspace for a small webhook endpoint inspector built
with Next.js App Router, React, TypeScript, Tailwind CSS, shadcn/Radix UI,
Drizzle ORM, PostgreSQL, Redis, PgBoss, and Vitest. It creates private endpoint
URLs, captures inbound HTTP requests, stores them in PostgreSQL, streams live
updates into a compact browser inspector, supports request replay and endpoint
forwarding, ships a whlol CLI under apps/cli for local forwarding, tailing,
and replay, and runs queued endpoint forwarding from the PgBoss worker app under
apps/pgboss.
Important domain terms are defined in CONTEXT.md. Read it before changing
webhook capture, persistence, event streaming, endpoint forwarding, request
replay, CLI transport, or browser endpoint-session code.
This repo optimizes for clean, correct, modern code. Best practices and high code quality are expected for new code and touched code.
- Follow current best practices for Next.js App Router, React, TypeScript, Tailwind CSS, Drizzle, and the repo's chosen libraries.
- Do not add shims, workaround layers, polyfill-style wrappers, fallback branches, compatibility adapters, or dual code paths unless the user explicitly requests them.
- Do not preserve old APIs, old props, old data shapes, stale flags, or dead abstractions "just in case."
- Do not take shortcut fixes, temporary hacks, bandaids, or "good enough for now" implementations and present them as finished work.
- Prefer replacing weak patterns over wrapping them.
- Fix root causes when reasonably possible. If a compromise is unavoidable, call it out clearly.
- Refactor locally when needed to keep the touched area coherent.
- Install dependencies:
pnpm install - Create database tooling env:
cp packages/database/.env.example packages/database/.env.local - Create web app env:
cp apps/web/.env.example apps/web/.env.local - Create docs app env:
cp apps/docs/.env.example apps/docs/.env.local - Create PgBoss worker env:
cp apps/pgboss/.env.example apps/pgboss/.env.local - Start local PostgreSQL:
pnpm db:local:start - Start local Redis:
pnpm redis:local:start - Apply migrations:
pnpm db:migrate - Start the dev server:
pnpm dev
The dev server runs on http://localhost:4665.
- Package manager: use
pnpm; do not switch to npm, yarn, or bun. - Dev server:
pnpm dev - Production build:
pnpm build - Production start after build:
pnpm start - Full local verification:
pnpm verify - Endpoint forwarding PgBoss worker in development:
pnpm pgboss:dev - Endpoint forwarding PgBoss worker after build:
pnpm pgboss:start - Format TypeScript and TSX files:
pnpm format
Keep route handlers and server-only modules on the Node.js runtime when they use
database access, Buffer, streams, or other Node-specific APIs.
Workspace task orchestration uses Turborepo. Keep internal workspace libraries
as compiled packages with dist package exports, and express dependency order,
caching, and long-running dev task behavior in turbo.json rather than in
nested shell scripts. Root app commands such as pnpm dev, pnpm web:build,
and pnpm pgboss:dev should use the Turbo-backed scripts so package builds and
watchers stay dependency-aware.
Use root Turbo-backed scripts for any app or worker command that depends on
compiled workspace packages. Direct filtered app commands such as
pnpm --filter @webhooks-lol/web build,
pnpm --filter @webhooks-lol/web test, or
pnpm --filter @webhooks-lol/pgboss build do not build ignored dist outputs
for workspace dependencies on a clean checkout. Use the root commands
(pnpm web:build, pnpm web:verify, pnpm pgboss:build,
pnpm pgboss:verify) or an explicit turbo run <task> --filter <app>...
invocation instead. Direct pnpm --filter <package> <script> remains
appropriate for package-owned commands that do not require compiled workspace
dependency outputs, such as database tooling and focused package tests after
the relevant dependency build graph has run.
Local env files live with the package or app that reads them. Put database
tooling variables in packages/database/.env.local, web runtime variables in
apps/web/.env.local, docs runtime variables in apps/docs/.env.local, and
PgBoss worker variables in apps/pgboss/.env.local.
Railway deployments use app-local config-as-code files:
apps/web/railway.json, apps/docs/railway.json, and
apps/pgboss/railway.json. Keep Railway service roots at the repository root so
pnpm workspace packages resolve correctly, and point each Railway service at its
own config file path. Do not add a root railway.json for this monorepo.
- Database table definitions live in
packages/database/src/auth-schema.tsandpackages/database/src/public-schema.ts;packages/database/src/schema.tsre-exports both for Drizzle and runtime setup. - Drizzle migrations live in
packages/database/drizzle/. - Generate migrations after schema changes with
pnpm db:generate. - Apply migrations with
pnpm db:migrate. - Use
pnpm db:pushonly for local prototyping, not as a replacement for a committed migration. - Open Drizzle Studio with
pnpm db:studiowhen inspecting local data. - Follow local database logs with
pnpm db:local:logs. - Stop local PostgreSQL with
pnpm db:local:stop.
Database access belongs behind the webhook repository layer. Do not put Drizzle queries directly in route handlers when the behavior belongs in a repository or domain module.
-
This repo deliberately uses a custom PostgreSQL schema for Better Auth while staying on the Better Auth Drizzle adapter. Standard Better Auth tables live in
auth(auth.user,auth.session,auth.account,auth.verification); app-owned tables live inpublic. -
Define Better Auth tables in
packages/database/src/auth-schema.tswithauthSchema.table(...). Define public app tables inpackages/database/src/public-schema.tswithpgTable(...). -
Keep
schemaFilter: ["public", "auth"]inpackages/database/drizzle.config.ts, and pass the schema object explicitly todrizzleAdapter. -
Do not add custom auth, role, setup-token, ownership, or bootstrap tables unless a Better Auth-supported feature and product design require it.
-
When Better Auth plugins change, generate CLI output as a table-shape reference, then apply the relevant changes to
packages/database/src/auth-schema.ts:cd apps/web pnpm dlx auth@latest generate \ --config lib/auth/schema-generator.ts \ --output .better-auth-schema.generated.ts \ --yes -
After changing table definitions, run
pnpm db:generateand confirm the migration keeps Better Auth tables inauthand app tables inpublic.
- Run all tests through package verification:
pnpm verify - Run web tests with package dependencies:
pnpm exec turbo run test --filter @webhooks-lol/web... - Run webhook core tests:
pnpm --filter @webhooks-lol/webhooks-core test - Run webhook server tests:
pnpm --filter @webhooks-lol/webhooks-server test - Run one package test file:
pnpm --filter <package> vitest run path/to/file.test.ts - Run package tests by name:
pnpm --filter <package> vitest run -t "test name" - Run TypeScript checks:
pnpm typecheck - Run app lint with package dependencies:
pnpm exec turbo run lint --filter @webhooks-lol/web... - Run package lint:
pnpm --filter <package> lint - Run the full suite before finishing broad changes:
pnpm verify
Tests live next to the package or app that owns the behavior. Web tests live
under apps/web/tests/, webhook core tests under packages/webhooks-core/tests/,
webhook server tests under packages/webhooks-server/tests/, and CLI tests
under apps/cli/tests/. Vitest runs in a Node environment. The web Vitest config
aliases @/ to apps/web and maps server-only to
apps/web/tests/server-only.ts; the server package maps server-only to
packages/webhooks-server/tests/server-only.ts.
Add or update focused tests for changed behavior. Repository and route-boundary changes should cover persistence rules, request parsing, error responses, and event publishing behavior as appropriate. Client session changes should cover state helpers, storage normalization, event-stream handling, and transport behavior. CLI changes should cover argument parsing, request shaping, SSE parsing/reconnect behavior, local delivery, replay selection, and API-client error handling as appropriate.
Do not add tests just to tick a coverage box. Tests should protect meaningful behavior, invariants, and ownership boundaries that would matter in a regression. Prefer concise table-driven coverage for input/output rules over narrow examples that only mirror the current implementation or assert third-party library internals.
The whlol CLI lives in apps/cli as a separate pnpm workspace package. It
owns local forwarding, tailing, replay command orchestration, API transport, SSE
parsing, request shaping, local delivery, and terminal output.
- Run CLI verification:
pnpm cli:verify - Run CLI tests:
pnpm --filter whlol test - Build CLI output:
pnpm --filter whlol build
Keep CLI-specific behavior inside apps/cli/src/* unless there is a durable
shared server/client contract that belongs in
packages/webhooks-core/src/api-contracts.ts.
Do not import browser endpoint-session code into the CLI.
The endpoint forwarding PgBoss worker lives in apps/pgboss. It is a separate
Node.js process that imports server-side forwarding behavior from
@webhooks-lol/webhooks-server/endpoint-forwarding/worker.
- Run the worker in development:
pnpm pgboss:dev - Build the worker and its package dependencies:
pnpm pgboss:build - Start the compiled worker:
pnpm pgboss:start - Verify the worker app and package dependencies:
pnpm pgboss:verify
Do not put production worker entrypoints under script/. Runtime processes
belong in apps/*; reusable implementation belongs in packages.
- TypeScript is strict. Keep it strict.
- Avoid
any, unsafe casts, and non-null assertions unless there is no reasonable alternative. - Model domain behavior with explicit types instead of stringly typed conventions.
- Keep unsafe input handling at boundaries and narrow data before passing it into domain code.
- Use the
@/import alias for app-local imports. - Follow the repo's Prettier config: no semicolons, double quotes, LF endings,
2-space indentation, trailing commas where configured, and Tailwind class
sorting through
prettier-plugin-tailwindcss. - Prefer small, cohesive modules with clear names over generic helpers.
- Do not scatter behavior into
utilsfiles without a real boundary or durable responsibility.
apps/web/app/api/**/route.tsfiles are server boundaries. They should parse route context, call dedicated server/domain modules, and shape HTTP responses.packages/webhooks-core/src/*owns shared webhook vocabulary and dependency-light contracts: endpoint IDs, captured request types, endpoint response shapes, request search helpers, endpoint forwarding enums/types, and API contracts.packages/webhooks-server/src/inbound-capture.tsowns inbound request capture rules, body limits, body parsing, captured-path/query/header extraction, and publication after persistence succeeds.packages/webhooks-server/src/repository.tsowns endpoint and captured-request persistence.packages/webhooks-server/src/endpoint-event-stream.tsowns live endpoint event-stream behavior.packages/webhooks-server/src/endpoint-forwarding/*owns server-side forwarding policy, target validation, delivery shaping, PgBoss queue integration, persistence, transport, and worker processing.packages/webhooks-server/src/request-replay/*owns replaying a stored captured request through the normal capture persistence/event publication path.packages/database/src/*owns Drizzle table definitions and PostgreSQL connection setup.apps/web/components/webhook-inspector/endpoint-session/*owns browser-side endpoint session state, storage, transport, and event-stream handling.apps/cli/src/*owns thewhlolcommand-line client.apps/web/components/ui/*contains shadcn/Radix-derived primitives. Extend them consistently instead of inventing incompatible UI primitives.
Prefer the flow Route Handler -> domain/server module -> repository for
database-backed behavior. Keep business rules out of UI components and route
handlers when they belong in domain modules.
- Prefer Server Components by default.
- Use Client Components only for state, event handlers, effects, custom hooks, or browser-only APIs.
- Use Route Handlers for HTTP endpoints, webhooks, SSE, and non-UI responses.
- Keep Route Handlers thin and explicit about cache behavior with
dynamic = "force-dynamic"and no-store headers when applicable. - Use Next.js App Router conventions and the Metadata API. Do not manage document head state manually in client code.
- Keep hooks focused on one responsibility and avoid hiding large workflows behind vague names.
- Match the existing inspector style: compact, practical, monospaced, information-dense, and restrained.
- Use shadcn/Radix primitives and
lucide-reacticons where appropriate. - Keep controls accessible: labels, focus states, keyboard behavior, and meaningful status text matter.
- Avoid marketing-page patterns for the app surface. This is an operational tool, so favor scanning, comparison, and repeated use.
- Do not add decorative UI that competes with request data.
- Redis-backed admission control protects endpoint creation, webhook capture request counts, captured body bytes, and live event-stream connection leases. Keep admission checks before expensive work such as body reads when possible.
- Policies live in
packages/webhooks-server/src/policies.ts, admission inpackages/webhooks-server/src/admission-control.ts, and Redis primitives inpackages/webhooks-server/src/rate-limits/*. - Event streams use Redis leases; renew with heartbeats and release on cleanup.
- Preserve CORS and no-store response behavior for capture endpoints.
- Browser preflight requests should not be saved as webhook traffic.
- Publish live request events only after persistence succeeds.
- Be careful with binary payloads: text display and base64 storage are separate concerns.
- Endpoint forwarding creates queued deliveries only after capture persistence succeeds. Forwarding workers must preserve original method, forwardable headers, body bytes, path mode, and query semantics while rejecting unsafe target URLs.
- Request retention must not delete captured requests with pending forwarding deliveries. Requests marked for deletion after forwarding should be pruned only after all pending deliveries for that request are no longer pending.
- Request replay creates and publishes a new captured request for the same endpoint. It should not enqueue endpoint-forwarding deliveries unless that behavior is explicitly changed.
- Add a new file when it creates a real module boundary or clarifies a durable responsibility.
- Extend an existing module when the new behavior is tightly related and not meaningfully reusable on its own.
- Choose names that describe domain responsibility, not the incidental task that led to the file.
- Before creating general-purpose helpers, look for a feature, domain, or layer where the behavior belongs.