This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Viewhook.dev (internal name: Viewhook) — a webhook testing application (like RequestBin/Webhook.site). Built with Laravel 12 + Inertia.js + React 18 + TypeScript. Tailwind CSS v4 for styling. SQLite for dev, TiDB Serverless (MySQL-compatible) in production.
All backend commands must run inside Sail:
./vendor/bin/sail composer run test # All checks: lint (Pint) + rector (dry-run) + types (PHPStan level 8) + tests (Pest with 100% coverage)
./vendor/bin/sail composer run lint # Pint style check (no fix)
./vendor/bin/sail composer run lint:fix # Pint auto-fix
./vendor/bin/sail composer run rector # Rector dry-run (check only)
./vendor/bin/sail composer run rector:fix # Rector auto-fix
./vendor/bin/sail composer run types # PHPStan static analysis
./vendor/bin/sail composer run test:unit # Pest unit tests only
./vendor/bin/sail composer run test:feature # Pest feature tests only
./vendor/bin/sail composer run test:both # Unit + Feature with 100% min coverageRun a single test by name:
./vendor/bin/sail ./vendor/bin/pest --filter=TestNamenpm test # All checks: eslint + prettier + tsc + unit tests
npm run lint # ESLint auto-fix
npm run lint:check # ESLint check only
npm run format # Prettier auto-fix
npm run format:check # Prettier check only
npm run types # TypeScript type check (tsc --noEmit)
npm run dev # Vite dev server
npm run build # Production buildUses Laravel Sail (Docker): ./vendor/bin/sail up -d
Important: All backend commands (composer, artisan, pest) must run inside Sail via ./vendor/bin/sail <command>. Ensure Sail is running before executing backend commands.
After modifying routes, regenerate TypeScript helpers: ./vendor/bin/sail php artisan wayfinder:generate (outputs to resources/js/wayfinder/). Use route.url() as a function call, not property access.
- Actions (
app/Actions/): Single-purpose action classes (main business logic entry points). Controllers delegate to these. - DTOs (
app/DataTransferObjects/): Immutable data containers with::fromRequest()/::fromArray()factories. - Services (
app/Services/): External I/O orchestration (export, rate limiting, webhook forwarding). - Jobs (
app/Jobs/): Async work —CleanupExpiredRequestsJob,ForwardWebhookJob,SendSlackNotificationJob. - Events (
app/Events/RequestCaptured): Broadcasts on private WebSocket channelurls.{resourceId}via Laravel Reverb.
- Inertia.js pages in
resources/js/Pages/, layouts inresources/js/Layouts/ - Vite alias:
@maps toresources/js/ - Real-time via
laravel-echo+pusher-jsconnecting to Laravel Reverb - Wayfinder-generated route helpers in
resources/js/wayfinder/
- Url model: Webhook endpoints identified by UUID
resource_id(used as route key). Captures requests atPOST|GET|PUT|... /catch/{uuid-or-slug}/{optional-path}. - Request model: Captured webhook data. Route key is
resource_id. - Capture flow:
CaptureController→CaptureRequestAction→CapturedRequestDTO→ save → broadcastRequestCapturedevent → optionally dispatch forwarding/notification jobs. - All models use
declare(strict_types=1), arefinal, and share aHasResourceIdconcern for UUID-based routing.
Laravel Sanctum + Breeze auth controllers.
- Framework: Pest PHP with Laravel plugin
- Test env (
phpunit.xml): SQLite in-memory, array cache, sync queue, null broadcast - Coverage: 100% minimum enforced. Auth controllers are excluded from coverage source.
- PHPStan: Level 8, covers
app/,database/,routes/ - Feature tests must be written from entry points only: HTTP endpoints (
$this->get/post/...), Artisan commands ($this->artisan()), or Jobs ($job->handle()). Never test Actions, Services, Form Requests, or Model methods directly in feature tests.
- Queue/Cache/Sessions: All database-backed (no Redis)
- WebSockets: Laravel Reverb
- Scheduler: Standard cron running
php artisan schedule:runevery minute
CRITICAL: Never expose the integer id column in public URLs, API responses, or frontend code.
- Internal
id: Integer primary key — used only for database relationships and internal queries - Public
resource_id: UUID — used in all URLs, API responses, and TypeScript types
// ✅ CORRECT: Expose resource_id as 'id'
public function toArray(Request $request): array
{
return [
'id' => $this->resource_id, // UUID string
'name' => $this->name,
];
}
// ❌ WRONG: Never expose internal ID
public function toArray(Request $request): array
{
return ['id' => $this->id]; // Exposes enumerable integer
}// ✅ CORRECT: id is always a UUID string in TypeScript
interface WebhookUrl {
id: string; // resource_id from backend
name: string;
}Models using HasResourceId: Url, Request (all public-facing models).
When adding a new public model: add migration for resource_id, add HasResourceId trait, update API Resource, update TypeScript type, regenerate Wayfinder routes.
| Layer | Responsibility | Can persist to DB? |
|---|---|---|
| Actions | Business logic, orchestrate within a request | Yes (via models) |
| Services | External I/O (HTTP calls, APIs) | No |
| Jobs | Async workflows | Yes (via model methods) |
| DTOs | Data transport between layers | N/A |
| Value Objects | Domain concepts with validation | N/A |
- Services orchestrate external I/O and return DTOs. They never write to the database directly.
- Jobs call services and then persist results via model factory/upsert methods.
- DTOs (
App\DataTransferObjects\) — many properties, transfer data across layer boundaries. - Value Objects (
App\ValueObjects\) — few properties, encapsulate domain validation (e.g., a slug format).
final readonly class CapturedRequestDTO
{
public function __construct(
public string $method,
public string $path,
public array $headers,
public string $body,
) {}
public static function fromRequest(IlluminateRequest $request): self
{
return new self(
method: $request->method(),
path: $request->path(),
headers: $request->headers->all(),
body: $request->getContent(),
);
}
}// Services orchestrate external I/O — no DB writes
final readonly class WebhookForwardingService
{
public function forward(WebhookRequest $request, string $targetUrl): ForwardingResultDTO
{
$response = $this->http->post($targetUrl, $request->body);
return ForwardingResultDTO::fromResponse($response);
}
}// Jobs call services then persist results
final class ForwardWebhookJob implements ShouldQueue
{
public function handle(WebhookForwardingService $service): void
{
$result = $service->forward($this->request, $this->request->url->forward_url);
$this->request->update(['forwarding_status' => $result->status]);
}
}When you make a mistake and the user corrects you, immediately propose capturing the fix as a permanent rule. Say:
"This looks like a pattern worth adding to the rules. Should I update
[file]?"
Then show the exact text to add. On approval, write it directly to the appropriate file:
.claude/rules/*.md— conventions that apply broadly across the codebase.claude/agents/*.md— corrections to how a specific agent behaves.claude/skills/*/SKILL.md— workflow improvements for a skill
Do this in the same response as the correction — not as a follow-up.
Conventional commit format:
<type>: <description>
<optional body>
Types: feat, fix, refactor, docs, test, chore, perf, ci
Attribution is disabled globally — do not add Co-Authored-By lines.