Skip to main content
Back to Portfolio

Inkwell

Form submission / inbound API — drop one HTML form tag, get explainable spam scoring, multi-destination fan-out, file safety, audit log, compliance endpoints, and a Filament admin where the buyer actually wants to live.

Laravel 13PHP 8.3MySQLRedisHorizonFilament 5Next.js 16ScalarTailwind 4OpenAPI 3.1
Inkwell preview

Overview

A portfolio build shaped for small businesses, marketing teams, and indie devs running a Webflow / Eleventy / static site who need a backend just for their contact form — and need it to do more than dump unfiltered spam into their inbox.

The integration is a plain HTML <form action="…">. No JavaScript required. The 3 KB optional widget adds inline error rendering and a page-render-timestamp signal but never changes the canonical path.

The headline move:

Every other form service hides the spam score. Inkwell ships the full per-signal breakdown in the admin and on the live demo — honeypot, IP reputation (StopForumSpam blocklist), timing, submission-rate, content density (URL count, all-caps ratio, phone density, body length), email validity (RFC + disposable-domain check), captcha passthrough. The buyer sees why a submission scored what it scored. Graduated friction (clean / quarantined-with-captcha / spam / hard-block) replaces the binary-block-or-let-through that competitors use.

Pipeline:

SpamSignal interface + composable pipeline registered in config/inkwell.php. Seven signals ship in v1, each one a small class that returns a SignalResult with points + per-signal metadata. Adding signal #8 is one class plus one config line plus a corpus row — no merge conflicts in the scorer. 18-case spam-scoring corpus at tests/corpus/spam-corpus.json with range-based assertions runs in every CI build.

Multi-destination fan-out:

Seven destinations in v1 — email (CRLF-strip + Reply-To + noreply sender), webhook (HMAC-SHA256 with 48-hour secret-rotation grace + SSRF guard), Slack (Block Kit), Discord (embed), Google Sheets (append-row), HubSpot (contact upsert by email), Mailchimp (audience members with datacenter-aware routing). Each destination is one class implementing the Destination contract. Dispatched in parallel via Horizon; per-(submission, destination, replay_seq) idempotency makes worker-crash retries safe. Slow OAuth connectors run on a separate queue from webhook/email/Slack so backpressure doesn't compound.

File safety:

Magic-byte verification via finfo against the form's MIME allowlist (Content-Type header is ignored — bytes only). Images pass through EXIF/IPTC/XMP strip so GPS coordinates on visitor-uploaded photos don't leak by default. ULID-keyed S3 paths; original_name preserved only for display. ClamAV scan runs out-of-band — submission acknowledges fast, infected files quarantine and the parent submission flips to quarantined for buyer review.

Compliance:

POST /v1/data-subjects/lookup returns every submission containing an email (without exposing the payload). DELETE /v1/data-subjects/by-email queues an async PurgeDataSubjectJob that cascades through submissions (payload null + meta.pii_purged = true + pii_purged_at set), submission_files (S3 + DB), delivery_attempts (response bodies → "[purged]"), with audit-row preservation via redacted diff. Daily inkwell:purge-old-submissions cron runs the 90-day retention horizon — IP redaction to /24 at the same mark.

Surface:

  • 23 OpenAPI operations under /v1/ covering submit + form CRUD + submission management + destinations + API keys + audit + data-subjects + idempotency
  • Submission endpoint is unauthenticated by design (the integration is HTML); rate limit + spam score + IP blocklist + explicit CORS allowlist (no implicit 7-day open window — that was a senior-review blocker, fixed) are the defence layers
  • Idempotency-Key header on every mutating management endpoint (Stripe-style 422 on body mismatch)
  • WorkspaceScope global Eloquent scope on every workspace-owned model — cross-workspace data leak is impossible at the query layer
  • audit_events table is INSERT-only via MySQL grant (documented for production)

Stack:

  • Laravel 13, PHP 8.3, MySQL 8, Redis, Horizon, Filament v5, Apache + mod_php, Let's Encrypt
  • Next.js 16 docs site with Scalar /reference, Tailwind 4 styling, working live-demo form
  • 3 KB optional JS widget at /widget/v1.js
  • 68 PHPUnit tests across spam scoring, ingest, destinations (with HTTP mocks), OAuth connectors, compliance endpoints, file-safety pipeline, idempotency, WorkspaceScope isolation

What it proves:

Same person hand-authored the OpenAPI spec, wrote the rule-based spam scorer + 18-case corpus, implemented seven destinations behind one contract, wrote the file-safety pipeline + ClamAV integration, built the compliance endpoints + PII-purge cron, scaffolded the Next.js docs site with the live-demo form proxy, deployed to EC2 with atomic releases + supervisord + PM2 + Let's Encrypt, and shipped it live. The case for hiring me to build a real form backend instead of stitching together Formspree + Zapier + a separate spam-check service.

Results

  • 68 PHPUnit tests covering spam scoring, ingest, destinations, OAuth connectors, compliance, file safety, idempotency, WorkspaceScope isolation

  • 18-case spam corpus with range-based assertions for tolerance to weight tuning

  • Seven destinations from one contract — email, webhook, Slack, Discord, Google Sheets, HubSpot, Mailchimp

  • Explainable scoring with per-signal breakdown rendered verbatim in the admin

  • PII-purge cron + data-subject lookup/erasure endpoints + audit-row redaction

  • Atomic release-based deploys via SSH + rsync to EC2; Horizon queue split (fast / slow) under supervisord

Gallery

Inkwell screenshot 2
Inkwell screenshot 3
Inkwell screenshot 4

Interested in working together?

Let's discuss how I can help with your project

Get in Touch