Zum Hauptinhalt springen
tsecurity.de LIVE
Echtzeit-Radar & Feeds
Alle RSS Feeds
👥 Community & Social
Windows Tipps & SecurityDave Plummer Has Made the Task Manager of Your Dreams(21.09.2026 um 21:20 Uhr)
Sichere ProgrammierungSubqueries and CTEs: Asking a Question Inside a Question(21.09.2026 um 21:00 Uhr)
Sichere ProgrammierungTVL Trend Analysis & Liquidity Risk Assessment: Lido(21.09.2026 um 21:00 Uhr)
Sichere ProgrammierungReact is Officially Dead in 2026 (Thanks to AI)(21.09.2026 um 21:01 Uhr)
Sichere ProgrammierungUsing SHA256 to Build Trustworthy Data Portals in Brazil(21.09.2026 um 21:01 Uhr)
Sichere Programmierung🚀 I reached 1,001 views on DEV!(21.09.2026 um 21:03 Uhr)
Sichere ProgrammierungReact Mental Models 2(21.09.2026 um 21:05 Uhr)
Sichere ProgrammierungAustralian RAM and SSD prices climb as stock tightens(21.09.2026 um 21:09 Uhr)
Windows Tipps & SecurityDave Plummer Has Made the Task Manager of Your Dreams(21.09.2026 um 21:20 Uhr)
Sichere ProgrammierungSubqueries and CTEs: Asking a Question Inside a Question(21.09.2026 um 21:00 Uhr)
Sichere ProgrammierungTVL Trend Analysis & Liquidity Risk Assessment: Lido(21.09.2026 um 21:00 Uhr)
Sichere ProgrammierungReact is Officially Dead in 2026 (Thanks to AI)(21.09.2026 um 21:01 Uhr)
Sichere ProgrammierungUsing SHA256 to Build Trustworthy Data Portals in Brazil(21.09.2026 um 21:01 Uhr)
Sichere Programmierung🚀 I reached 1,001 views on DEV!(21.09.2026 um 21:03 Uhr)
Sichere ProgrammierungReact Mental Models 2(21.09.2026 um 21:05 Uhr)
Sichere ProgrammierungAustralian RAM and SSD prices climb as stock tightens(21.09.2026 um 21:09 Uhr)
Intelligence View
⚡ tsecurity.de Intelligence

Building an In-App E-Signature Flow Without DocuSign (Laravel + React + Canvas)

A customer of mine was printing offer letters, signing them with a pen, scanning them on a flatbed, and emailing them back. In 2026. Inside an app I built for him. So last week I shipped a proper offer-letter workflow with an in-browser…

0
↗ Quelle (dev.to)
Reagiere als Erste:r — dein Feedback zählt!

A customer of mine was printing offer letters, signing them with a pen, scanning them on a flatbed, and emailing them back. In 2026. Inside an app I built for him.



So last week I shipped a proper offer-letter workflow with an in-browser e-signature surface — no DocuSign, no third-party embed, no bolt-on fees. Just a <canvas>, a signed payload, and a stored PDF.



Here's what I learned. The stack is Laravel 12 + Inertia v2 + React 19 + Mantine + Tailwind v4, but most of this is transferable.






The problem in one paragraph



An offer letter flow has four moving parts: (1) a templated document with variables (name, salary, start date), (2) a public URL a candidate can open without logging in, (3) a signing surface that works on phone and desktop, and (4) a signed PDF that gets archived and is legally defensible. None of these are hard individually. The interesting part is where they collide.






1. The public signing URL — stop being clever



First instinct: JWTs. Signed URLs. Short expiry. A whole dance.



What I actually shipped: a random 64-char token on the offers table, looked up on a route-model-binding where('token', $token)->firstOrFail(). That's it.




// routes/web.php
Route::get('/offer/{token}', [CandidateOfferController::class, 'show']);
Route::post('/offer/{token}/accept', [CandidateOfferController::class, 'accept']);






The token is the secret. No login, no account creation, no email confirmation round-trip. The candidate clicks the link and sees the letter. That's the entire auth model for the candidate side.



If you're thinking "but what about security" — the token has ~380 bits of entropy, the route is HTTPS only, and every action is idempotent and logged. DocuSign's own "anyone with the link can sign" flow has the same threat model. Don't over-engineer this.






2. The signature canvas — dark mode will bite you



I used react-signature-canvas, which wraps the excellent signature_pad library. The naive integration:




<SignatureCanvas
penColor="var(--mantine-color-text)"
canvasProps={{ className: 'w-full h-48 border rounded' }}
/>






This silently fails in dark mode.



The <canvas> 2D context cannot resolve CSS custom properties. It sees var(--mantine-color-text) as a literal string, fails to parse it, and falls back to... nothing visible. The user draws, the strokes are captured, but they're rendered in a color the canvas can't compute. You get a "signature captured!" toast and a completely blank canvas.



The fix is to resolve the color at runtime:




import { useComputedColorScheme } from '@mantine/core';

const colorScheme = useComputedColorScheme('light', { getInitialValueInEffect: true });
const penColor = colorScheme === 'dark' ? '#f1f3f5' : '#1a1a1a';
const canvasBg = colorScheme === 'dark' ? '#1a1b1e' : '#ffffff';

<SignatureCanvas
penColor={penColor}
backgroundColor={canvasBg}
canvasProps={{ className: 'w-full h-48 border rounded' }}
/>






Canvas doesn't know about CSS. Always pass hex. This cost me an embarrassing amount of debugging time.






3. Type-to-sign — the 40% of users who hate drawing



Half your users are on a laptop with a trackpad. Drawing a signature with a trackpad looks like a seismograph readout. You need a second input mode: type your name, render it in a cursive font.




const TYPED_FONT = '"Dancing Script", "Brush Script MT", "Lucida Handwriting", cursive';

function renderTypedSignature(canvas: HTMLCanvasElement, text: string) {
const ctx = canvas.getContext('2d')!;
ctx.clearRect(0, 0, canvas.width, canvas.height);

let fontSize = 64;
ctx.font = `${fontSize}px ${TYPED_FONT}`;

// Auto-fit: shrink until it fits
while (ctx.measureText(text).width > canvas.width - 40 && fontSize > 24) {
fontSize -= 2;
ctx.font = `${fontSize}px ${TYPED_FONT}`;
}

ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = penColor;
ctx.fillText(text, canvas.width / 2, canvas.height / 2);
}






Two gotchas here:



1. Load the font explicitly. Don't assume Dancing Script is on the user's system. Load it via <Head> in Inertia:




<Head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link
href="https://fonts.googleapis.com/css2?family=Dancing+Script:wght@600&display=swap"
rel="stylesheet"
/>
</Head>






2. Export the same format as the drawn version. Call canvas.toDataURL('image/png') in both modes. Your backend then has a single format to handle. The user picks between Draw and Type via a <SegmentedControl>, and the output is identical from the server's perspective.






4. The signed payload — keep it boring






OfferSignature::create([
'offer_id' => $offer->id,
'signer_name' => $request->input('signer_name'),
'signer_email' => $request->input('signer_email'),
'signature_data' => $request->input('signature_data'), // PNG dataURL
'ip_address' => $request->ip(),
'user_agent' => $request->userAgent(),
'signed_at' => now(),
]);

$offer->update([
'status' => OfferStatus::Accepted,
'accepted_at' => now(),
]);






IP + user agent + timestamp is the legal audit trail. You don't need a blockchain. You don't need KBA. For employment offers, this is the standard the US ESIGN Act and EU eIDAS Simple Electronic Signature actually require. Ask a lawyer for your jurisdiction before you believe me, but the legal bar is much lower than the SaaS marketing suggests.






5. The signed PDF — generate it, store it, forget it



Once the candidate signs, kick off a job that:




  1. Renders the letter HTML to PDF (I use spatie/browsershot).

  2. Embeds the signature image at the bottom with the signer's name, typed timestamp, and IP.

  3. Stores it in object storage (Cloudflare R2 in my case — free egress is the whole game).

  4. Makes it downloadable from both the employer dashboard and the candidate's public URL.




class GenerateOfferPdfService
{
public function generateAndStore(Offer $offer): string
{
$html = $this->renderHtml($offer);
$pdf = Browsershot::html($html)->format('A4')->pdf();

$path = "offers/{$offer->id}/signed.pdf";
Storage::disk('r2')->put($path, $pdf);

return $path;
}
}









6. The pipeline trigger — where the magic actually is



Here's the bit that separates "e-signature tool" from "offer-letter workflow." When the candidate signs, I also fire a state transition on the job application:




public function execute(Offer $offer, array $signatureData): void
{
// ... create signature, update status ...

$application = $offer->jobApplication;
if ($application) {
$hiredStage = $application->job->stages()
->where('slug', 'hired')
->first();

if ($hiredStage) {
$application->update(['status_id' => $hiredStage->id]);
}
}
}






The candidate slides from "Offer Sent" to "Hired" in the Kanban board the moment they sign. The employer doesn't touch anything. This one line of code is the reason "offer letter as a feature of the ATS" is 10x better than "offer letter as a standalone SaaS."






The lesson



The interesting part of building this wasn't any single piece. It was that the seams between pieces are where products actually differentiate. A standalone e-signature tool can't move a candidate to "Hired" because it doesn't know what a pipeline is. A standalone ATS can't reliably track a signed offer because it doesn't own the signing surface. Owning both means you get to delete five manual steps from the workflow, and nobody else can.



If you're building in a mature SaaS market and you can't find a differentiator — look at the integrations your users are duct-taping together, and build one of them natively. The seams are where the leverage lives.






I'm Zakir, solo founder of JuggleHire. We shipped this feature yesterday and launched it on Product Hunt today. If you want to see the flow in action or roast my code, the app is at jugglehire.com.



Non-technical write-up on the "why" is on my blog.

Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Building an In-App E-Signature Flow Without DocuSign (Laravel + React + Canvas)

Thematisch verwandte Begriffe: Building, InApp, ESignature, Flow · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-77582 | Tinyauth is an authentication and authorization server. Prior to 5.1.0, …
Advisory →
TTS Reader • tsecurity.de Voice
tsecurity.de Icon
tsecurity.de App
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag
Themen-Radar & Intelligence Matrix
Echtzeit-Taxonomie nach Angriffsvektoren & Plattformen

tsecurity.de Live Threat Radar

🔴 LIVE RADAR
MONITORING
AKTIV
CVE-DATENBANK
LIVE
🔍
Community Radar & Live Chat
Sentinel Bot online • Live-Stream
Dein Cluster: Security Explorer
Match:
lädt…
Verbindung zum Community-Stream wird aufgebaut...
Bearbeitungsmodus — Senden überschreibt deine Nachricht
Community-Puls — was gerade passiert
lädt…
Aktivitäten deiner Analysten
lädt…
Neues Thema oder Eilmeldung einreichen

Reiche interessante Links, Zero-Days oder Debatten ein. Die Community entscheidet per Upvote über die Veröffentlichung.

Heiß diskutierte Einreichungen
🔖 Gespeicherte Artikel
📂 Keine gespeicherten Artikel vorhanden.
Zurück Ziehen Vor
Links: vorheriger Artikel Rechts: nächster Artikel unten: schließen
News NIS-2 Frühwarnung Tier-1 Intel ⏱️ 3 Min vor 10 Min
Artikeldaten werden geladen...

Zurück: vorheriger Vor: nächster
↗ Original-Quelle
Social Reaktionen Deine Reaktion zählt
Einstufung & Relevanz-Poll 0 Stimmen
In sozialen Netzwerken teilen 1-Klick