How I Added a Poaching Clause to My Client Contracts in a Laravel 12 + Next.js Stack
Why I Needed a Poaching Clause
A few weeks ago, my sales team flagged a recurring issue: clients were hiring my on-site developers directly after project completion. While flattering, it undercut my delivery model and hurt team retention. The fix? A clear poaching clause in my client contracts.
This wasn’t just a legal tweak—it was a full-stack update. My primary product, AustinsElite, runs on Laravel 12 with a modern Laravel 12 frontend. Contract terms appear in two places: a user-editable form powered by Laravel Livewire (rendered in the Laravel 12 interface), and a PDF generated server-side for e-signature. Any change had to be reflected in both—consistently and accurately.
The challenge? Keep the UX smooth, maintain document integrity, and ship fast. Here’s how I did it.
Updating the Livewire Form in the Next.js Frontend
Even though my frontend is built with Next.js, the contract form itself is a Livewire component embedded via Inertia.js (a setup I’ve found perfect for gradual modernization of Laravel apps). That meant the form logic and state lived in Laravel, but the rendering happened in React.
My first step was to add the new clause as a checkbox in the Livewire component (EditContract). I didn’t want to hardcode it—this might not be the last legal addition—so I abstracted contract clauses into a config-driven array:
// config/contract_clauses.php
return [
'poaching' => [
'label' => 'No Poaching Agreement',
'text' => 'Client agrees not to hire or solicit any member of the development team for 12 months.',
'required' => true,
],
];
Then, in the Livewire view:
@foreach(config('contract_clauses') as $key => $clause)
<div class="clause">
<x-checkbox
wire:model="clauses.{{ $key }}"
:required="$clause['required']"
/>
<label>{{ $clause['text'] }}</label>
</div>
@endforeach
This approach let me toggle clauses globally or per-contract type later. The Next.js layer didn’t need deep changes—just a re-render when the Livewire component updated. Inertia handled the data sync seamlessly.
I also added client-side validation using Laravel’s native rules, ensuring the clause was acknowledged before submission. No extra JavaScript libraries, no race conditions—just clean, predictable behavior.
Syncing the PDF Template for Consistency
The real gotcha? The PDF. I generate it server-side using DomPDF and a Blade template (contract.blade.php), which pulls the same contract data from the database. If the web form showed the clause but the PDF didn’t, I’d have a legal nightmare.
So I mirrored the config change in the PDF template:
<!-- resources/views/pdf/contract.blade.php -->
@foreach(config('contract_clauses') as $clause)
@if($contract->hasClause($key))
<div class="pdf-clause">
<strong>{{ $clause['label'] }}:</strong>
{{ $clause['text'] }}
</div>
@endif
@endforeach
I added a hasClause() method to my Contract model to support conditional inclusion (e.g., for enterprise clients who might have negotiated it out). This kept the logic centralized and testable.
To verify parity, I wrote a quick feature test that renders both the Livewire form and the PDF for the same contract and checks for the presence of the clause text. One test, two outputs validated.
$this->assertStringContainsString('No Poaching Agreement', $livewireHtml);
$this->assertStringContainsString('No Poaching Agreement', $pdfText);
It’s not glamorous, but it’s bulletproof.
Shipping Fast Without Sacrificing Integrity
This wasn’t a flashy feature—no animations, no new APIs. But it mattered. Legal compliance is part of the product, especially in B2B SaaS. By treating contract terms as first-class application data (not static text), I made future updates faster and safer.
The hybrid Laravel 12 + Next.js stack gave me the best of both worlds: Laravel’s rapid backend iteration and form handling, and Next.js’s modern frontend. Livewire kept the interactivity tight, while the shared config ensured consistency.
Now, when legal comes knocking with another clause—NDAs, IP ownership, you name it—I’m ready. One config update, two templates in sync, and I’m done.