Senior Software Engineer · Laravel & React
Malik
Alleyne-Jones
I build the parts of a product that move money and can’t be wrong twice: Stripe Connect payouts, real-time bidding, and billing engines where no two customers pay the same price. Seven years of it, most recently at Kirschbaum, where the lead-distribution platform I work on has processed $15M+ in lead revenue.
Columbus, Ohio, United States
Experience · 2019 — present
The record
Five roles across three employers, in the order they happened. The books each one produced are on the shelf below.
-
Jul 2024 — present
Senior Software Developer · Kirschbaum
Remote
- Ship features on PRESSED*, a lead-distribution platform that has processed $15M+ in lead revenue and 242k leads: real-time bidding, a reverse-auction marketplace, and delivery over webhooks, email, and SMS.
- Designed an AI onboarding agent with 14 tenant-scoped tools behind a phase-gated state machine.
- Automated white-label domain provisioning across ACM, CloudFront, and Route 53, cutting tenant DNS setup to a single CNAME.
- Own affiliate attribution and the embeddable lead-capture forms on TrailTag*, including partner postbacks that fill each network's own macros at send time.
- Consolidated billing on LedgerHound* behind one price-resolution service, then rebuilt affiliate-group billing so a parent organization pays one invoice while each member company is priced at its own tier.
- Closed a discount-code hole in a prop-trading challenge checkout, where the query scope and the validator disagreed on what counted as valid and a code issued to one trader still worked in anyone else's cart.
- Build the React Native front end of an unreleased restaurant-discovery app: a two-detent detail sheet with a pinned action bar and routed sub-sheets, plus a friend-map mode gated server-side by an accepted follow.
- Laravel
- Inertia
- React
- React Native
- Livewire
- Filament
- Stripe
- MySQL
- AWS
-
Apr 2024 — Jul 2024
Lead Software Engineer · Birdboar
Lexington, KY · Hybrid
- Came back to Blue Horse Entries for a season of hardening: event archiving, a StartBox rides resource, and the fix that stopped abandoned carts from holding stabling capacity.
- Backfilled the test suite around fees and orders, where a quiet regression costs an organizer real money.
- Laravel
- Livewire
- Pest
-
Jul 2023 — Jun 2024
Computer Programmer · Fayette County Public Schools (KY)
Lexington, KY
- Built Pinwheel solo, a three-tier personnel and budget approval workflow that has routed $100k+ in requests, with dual designation so one person can submit on one request and approve on the next.
- Laravel
- Livewire
- Filament
-
Nov 2021 — Dec 2023
Lead Software Engineer · Birdboar
Lexington, KY
- Led engineering on Blue Horse Entries, an equestrian show discovery and registration platform: 1,200+ shows hosted, 48k entries processed, 99.9% uptime. I wrote most of the codebase.
- Shipped Race Passport with EPM Productions: 300+ races listed, 25k runners registered, entry through payment in one flow.
- Partnered with BlueCheck across three identity-verification services: PHPUnit coverage that caught a name-matching bug passing the wrong person, then Sanctum API tokens issued to merchant domains rather than users.
- Laravel
- Livewire
- Stripe
- PHPUnit
-
Aug 2019 — Nov 2021
Systems Analyst · Fayette County Public Schools (KY)
Greater Lexington Area
- Delivered Planbook: weekly preschool lesson planning with standards tagging and Sunday-evening parent digests.
- Shipped Dispatch, a call console for the district police department that parses radio traffic as it is typed, so codes attach and priorities escalate without a second form to fill in.
- Built the student-facing half of Elevate, a college- and career-readiness platform: profiles, the evidence workflow behind each milestone, teacher dashboards, and the shared form and slideover components the rest of the app was built on.
- Laravel
- Livewire
- Alpine
Selected work · 2019 — present
The shelf
Eleven books in three slipcases, shelved by employer. Pull one open by its spine and you get the whole case study, numbers and all. Three are marked Start here if you only have a minute.
Some product and client names on this shelf have been changed. The work, the numbers, and the code are real.
PRESSED* · Kirschbaum
Squeeze Every Lead
Lead distribution with real-time bidding, a private marketplace, and delivery in seconds
One of the engineers on the platform since 2024. The three problems below are the ones I own: webhook delivery that stops double-firing under retries, the AI onboarding agent, and custom-domain provisioning.
- Laravel
- Inertia
- React
- lead revenue processed
- $15M+
- leads processed
- 242k
Bid
Clients fund wallets and compete for leads under caps and limits, paying only what it takes to win.
Auction
Unsold leads land in a private reverse-auction marketplace where the price drops until someone buys.
Deliver
Winners get their leads over webhooks, email, and SMS, routed by their own filters and rules.
“Every feature in PRESSED exists because we’ve lived the pain of not having it.”
- Duplicate protection
- Lead audit rules
- Return rate tiers
- Demand analytics
- White-label domains
- Wallet auto-recharge
Under the hood
Three of these look routine on a feature list and were not: an async pipeline against a slow third party, an agent that cannot reach across tenants, and a timing window that took four attempts to close.
Webhook delivery idempotency under concurrency
MySQL · queue semantics
Retried jobs were double-firing tenant webhooks. Closing the window took four attempts, and three of them were wrong.
60 < 75 < 90
Read
Close
Webhook delivery idempotency under concurrency
Retried jobs were double-firing tenant webhooks. Closing the window took four attempts, and three of them were wrong.
Problem
FireWorkflows is a queued listener that Horizon retries up to five times, and it
was not idempotent. A single lead also legitimately emits several events, each queueing its own
job. Put those together and a customer's endpoint could receive the same lead twice. For anyone
billing on delivered leads, that is not a cosmetic bug.
Approach
The obvious fix is a claim table: unique(workflow_id, dedup_key), claim a row
inside a short row-locked transaction, then send outside the lock so a slow HTTP call is not
holding a database lock open the whole time.
$delivery = DB::transaction(function () use ($workflowId, $dedupKey, $leadId): ?WorkflowWebhookDelivery {
$existing = WorkflowWebhookDelivery::query()
->where('workflow_id', $workflowId)
->where('dedup_key', $dedupKey)
->lockForUpdate()
->first();
if ($existing) {
return $this->resolveExistingClaim($existing);
}
return WorkflowWebhookDelivery::create([
'workflow_id' => $workflowId,
'dedup_key' => $dedupKey,
'claimed_at' => now(),
// ...
]);
}, attempts: 3);
// The send happens outside the lock. Confirm the claim on success; release it on
// failure, so a genuine retry still has something left to deliver.
if ($this->sendWebhook($exchangeConfiguration, $data)) {
$delivery->update(['sent_at' => now()]);
return;
}
$delivery->delete();
Claiming before sending is only half of it. If the send fails transiently, the claim has to be released or the retry has nothing left to deliver, and releasing means a redelivery can reclaim a claim that is still mid-send. So an unconfirmed claim became a lease.
public const CLAIM_LEASE_SECONDS = 75;
protected function resolveExistingClaim(WorkflowWebhookDelivery $existing): ?WorkflowWebhookDelivery
{
if ($existing->sent_at !== null) {
return null;
}
$leaseCutoff = now()->subSeconds(self::CLAIM_LEASE_SECONDS);
if ($existing->claimed_at === null || $existing->claimed_at->lessThan($leaseCutoff)) {
$existing->update(['claimed_at' => now()]);
return $existing;
}
// A fresh unconfirmed claim means another attempt is still in flight.
return null;
}
That constant is where it got interesting. It started at 10 minutes, picked to sit comfortably
above a ~180s worst-case send. That was wrong. The Horizon worker timeout is 60s, so a worker
killed mid-send never reaches its delete-on-failure and orphans the claim. Every redelivery
inside those 10 minutes then read the orphan as fresh and skipped it, and the delivery was lost
permanently. Recalibrating to 150s was also wrong, for a subtler reason: a listener that skips
still returns normally and gets ACKed, so an orphaned claim gets exactly one shot at
recovery: the single redelivery at retry_after.
Outcome
75 seconds. Above the 60s worker timeout, so a live attempt is never mistaken for a crash;
below the 90s retry_after, so a dead worker's orphan is always already stale by
the time the queue redelivers it.
60 < 75 < 90. Every number in that chain is load-bearing, and two of the
three live in config the lease does not own, so the boundary tests derive their values from the
constant rather than hard-coding them. The unique constraint stayed as defense in depth,
deadlocks under InnoDB gap locks get absorbed by attempts: 3 instead of throwing out
of handle(), and the claim table is MassPrunable on a short retention
window.
Automated custom-domain provisioning
AWS · ACM → CloudFront → Route53
A tenant types their domain and pastes one CNAME. Doing it by hand meant five.
5 records → 1
Read
Close
Automated custom-domain provisioning
A tenant types their domain and pastes one CNAME. Doing it by hand meant five.
Problem
Tenants run PRESSED on their own domain. On AWS that means requesting an ACM certificate, standing up a CloudFront distribution, reading the DNS validation options back off the certificate, getting the tenant to prove ownership, waiting on issuance, then relinking the distribution with an SNI certificate.
Two things make that hostile to a form submission. ACM is eventually consistent, so the validation options do not exist the moment you request the certificate, and issuance can take an hour. And the naive version hands the tenant five DNS records to paste into a registrar they may not have logged into in a year.
Approach
A chained job pipeline, dispatched off DomainCreated.
$response = $this->service->requestCertificate($event->domain);
if ($response->get('CertificateArn')) {
$event->domain->update(['certificate_arn' => $response->get('CertificateArn')]);
Bus::chain([
new CreateDistribution($event->domain),
function () use ($event) {
RetrieveValidationOptions::dispatch($event->domain)->delay(now()->addSeconds(5));
MaskValidationOptions::dispatch($event->domain)->delay(now()->addSeconds(7));
},
])->dispatch();
}
Each step owns its own patience. Jobs release themselves back onto the queue rather than throwing, so a slow-but-healthy AWS never looks like a failure. The retry horizon is tuned per step, because "the validation options have not appeared yet" and "the certificate has not been issued yet" are a minute and an hour apart.
public function handle(): void
{
$response = $this->service->describeCertificate($this->domain);
// ...
if ($this->passes($response)) {
return;
}
if ($this->failed($response)) {
$this->domain->update(['error_message' => 'Domain validation failed.', 'error_at' => now()]);
$this->fail(new Exception('Domain validation failed.'));
return;
}
// DNS propagation in progress - retry without throwing exception
if (now()->isBefore($this->retryUntil())) {
$this->release($this->backoff());
}
}
public function backoff(): int
{
return 2;
}
public function retryUntil(): DateTime
{
return now()->addMinute();
}
MaskValidationOptions is the step that saves the tenant. Rather than forwarding
ACM's validation records, it writes them into PRESSED's own Route53 zone and points a per-tenant
hostname at the distribution, so the tenant only ever sees one record.
Outcome
One CNAME to paste. Underneath it, the parts nobody sees: a CloudFront viewer-request function
that copies the viewer Host header through so the app can tell which tenant it is
serving, proxy trust for CloudFront, session domains resolved per tenant, and the scheme and
root URL forced to match.
Teardown runs the same service in reverse. LinkDomainToDistribution sweeps up the
tenant's soft-deleted domains as its last act, so switching domains cleans up the old one
without anyone having to remember. Documented in
docs/development/10-custom-domains.md.
AI onboarding agent
Laravel AI SDK · 14 tenant-scoped tools
An agent that builds a tenant's configuration for them, and structurally cannot touch anyone else's.
14 tools · 0 tenant params
Read
Close
AI onboarding agent
An agent that builds a tenant's configuration for them, and structurally cannot touch anyone else's.
Problem
A new tenant lands on an empty panel and has to produce lead categories, lead types, bidding rules, and distribution stages before anything works. That is a great interview for an agent to run.
It is also a new attack surface. Every tool is an endpoint the model can be talked into calling,
and any tenant_id the model gets to supply is a tenant_id it can get
wrong. Most agentic demos skip this, because the demo has one tenant.
Approach
Two gates, and neither of them trusts the model. The first is that the tenant is never a parameter. It resolves from the authenticated user, and every query filters on it, so a coaxed or stale id does not fetch someone else's record. It fetches nothing.
/**
* The tenant is always resolved from the authenticated user - it must never
* be supplied by the AI as a parameter.
*/
trait ResolvesTenant
{
protected function resolveTenantId(): ?int
{
return auth()->user()?->tenant_id;
}
}
// ...and every lookup inside a tool is filtered by it, so a wrong id finds nothing:
$category = LeadCategory::query()
->where('tenant_id', $tenantId)
->find($validated['lead_category_id']);
if (! $category) {
return ToolResult::error("Lead category with ID {$validated['lead_category_id']} was not found for this tenant.");
}
The second gate is progressive tool disclosure. A state machine derives the current phase from
the session's completed_phases and hands back only that phase's tools. The agent
cannot call ConfigureBidding before discovery is finished, because that tool was
never in the payload. Nobody had to tell it no.
private const PHASE_TOOLS = [
'discovery' => [AnalyzeCsvLeads::class, ParseUploadedCsv::class, MapCsvFields::class, /* ... */],
'category_setup' => [CreateLeadCategory::class, CompletePhase::class, /* ... */],
'lead_type_setup' => [CreateLeadType::class, CompletePhase::class, /* ... */],
'bidding' => [ConfigureBidding::class, CompletePhase::class, /* ... */],
'distribution' => [CreateDistributionStage::class, ScaffoldSource::class, /* ... */],
'complete' => [GetDocumentationLinks::class, RecordMemory::class],
];
/**
* Only these tool instances should be passed to the AI provider - any tool
* not in this list is inherently unavailable to the agent for this turn.
*/
public function toolsForPhase(OnboardingPhase $phase, ?OnboardingSession $session = null): array
{
$toolClasses = self::PHASE_TOOLS[$phase->value] ?? [];
// ...
}
Phase is derived rather than stored, so there is no separate column to drift out of sync with the thing it describes.
Outcome
14 tools across five phases. Around them: layered system prompts versioned in the database behind an admin CRUD, so changing a prompt is a record rather than a deploy; conversation memory; created-model tracking so the agent knows what it already built; sanitized input; and an observability page showing every conversation, tool invocation, and raw payload. Everyone asks the same first question about an agent in production, which is what it actually did, and the answer to that should not be a log grep.
LedgerHound* · Kirschbaum
Somebody Has To Get The Invoice Right
Sales tax filing, and the billing engine underneath it
I own the money side rather than the filings: the pricing services, the checkout quote, tiered subscriptions for parent organizations billing on behalf of their member companies, and the Stripe reconciliation that keeps the invoice agreeing with the database.
- Laravel
- Livewire
- Filament
- Stripe
- seeded jurisdictions
- 153
- tests
- 103
Quote
The checkout wizard prices a jurisdiction set live, against whatever rate that customer is actually on.
Charge
Subscriptions, one-time fees, and per-return charges land on the right Stripe customer by card or ACH.
Reconcile
Webhooks sync every invoice back, and a monthly pass rewrites the pending lines so drift cannot accumulate.
- Tiered subscriptions
- Affiliate group billing
- Grandfathered pricing
- Card & ACH payments
- Coupons & promotion codes
- Credit adjustments
- Payment retry
- Invoice webhook sync
Under the hood
Filing the return is the product. My half is deciding what it costs — and in a business where half the customers are on a rate somebody promised them years ago, that turns out to be the half with the interesting failure modes.
One price, four places it could come from
PriceService · Stripe · Spatie settings
The quote and the invoice were computed by different code. Only one of them knew the customer.
4 stores → 1 answer
Read
Close
One price, four places it could come from
The quote and the invoice were computed by different code. Only one of them knew the customer.
Problem
A price here is never just a price. A customer might sit in a pricing group with its own subscription rate; they might match a conditional override inside that group; they might be grandfathered onto a legacy rate captured in a JSON column years ago; and failing all of that, there is a default in the settings table. Four stores, and the answer has to be the same everywhere it is asked for.
It wasn't. Before this, three call sites resolved prices independently, and two of them — the checkout summary and the order biller — read straight from the settings defaults with no team argument at all. Only the return-creation path checked the legacy override. Nothing in the repo records a customer being misbilled over it, and I want to be precise about that. But the shape of the code meant a grandfathered customer could be quoted at one rate and billed at another, and nobody would find out until someone read an invoice closely.
Approach
One service, one entry point, and a precedence written down in the docblock so the next person does not have to infer it from the branch order.
/**
* Get the Stripe Price ID for a product, respecting pricing group and legacy tier overrides.
* Resolution: PricingGroup price → legacy tier override → service settings default
*/
public function getPriceId(Team $team, string $productKey, ?string $lookupKey = null): string
{
// Check for pricing group override
$groupPriceId = $this->getGroupPriceOverride($team, $lookupKey ?? $productKey);
if ($groupPriceId) {
return $groupPriceId;
}
// Check for legacy tier override
$legacyPriceId = $this->getLegacyOverride($team, $productKey);
if ($legacyPriceId) {
return $legacyPriceId;
}
// Fall back to default pricing
return $this->getDefaultPriceId($productKey, $lookupKey);
}
Both public methods take a Team. That is the whole design, really —
you cannot resolve a price without saying who it is for, so the class of bug where
a caller forgets the customer stops being expressible.
The conditional layer underneath is the part I would keep. Override rows carry conditions dispatched through a small registry — account count, monthly usage, subscription age, membership in an affiliate group — AND-ed together, ordered by priority, first match wins. Adding a pricing rule is a row and a condition class, not a deploy of the resolver.
Outcome
The callers that bill now agree with the callers that quote. The old
per-model helper is deprecated with no callers left, and a pair of artisan
commands moves legacy tiers into the newer schema with a --dry-run
first.
What I would not claim is that it is finished. Three call sites still resolve prices outside the service at time of writing, one of them computing a services total that can disagree with its own line items, and there is no automated test over price resolution at all. It is a consolidation, not a completed migration, and the honest next step is characterization tests before anything else moves.
Billing a parent for children that price differently
Cashier · Stripe invoice items
A holding company pays one bill. Every company under it qualifies at its own tier.
$0 anchor + N line items
Read
Close
Billing a parent for children that price differently
A holding company pays one bill. Every company under it qualifies at its own tier.
Problem
Some customers are a parent organization that pays on behalf of member companies. The parent wants one invoice. Each member independently qualifies for its own tier, based on how many active accounts it has.
The original model expressed that as subscription-item quantities: a
price per tier, and the quantity was how many member companies landed in that
tier. It reads fine until you notice what it costs. Members were bucketed by
tier, and the price for a bucket was looked up from one representative member —
$teams->first() — so everyone in a bucket silently inherited the
first company's overrides. A grandfathered member could pull its whole bucket
onto a legacy rate, or lose its own.
Approach
Flip it around. The group's subscription becomes a $0/month anchor that exists only to hold a billing cycle, and every member's fee becomes its own pending Stripe invoice item on the parent's customer, priced from that member's pricing group.
public function resolveReturnSubscriptionTierAmount(Team $team, int $totalAccounts): int
{
if (! $team->currentUsageOverview['qualifiesForSubscription']) {
return 0; // Tier zero logic
}
$group = $team->pricingGroup();
// Try pricing group's custom tiers first
if ($group) {
$tiers = $group->getStripeTiers();
if ($tiers->isNotEmpty()) {
$matchingTier = $tiers->first(fn ($tier) => is_null($tier['up_to']) || $totalAccounts <= $tier['up_to']);
return $matchingTier ? $matchingTier['flat_amount'] : 0;
}
}
// ... falls back to expanding tiers on the default subscription price
}
This is the real trade, and it is worth being blunt about it: we gave up Stripe's tier engine. The tiered price object is now read as a lookup table — expand the tiers, find the first band the account count fits — and the flat amount is computed in PHP and sent to Stripe as a plain integer. Per-member line items cost us the thing Stripe was doing for free.
// Resolve tiered flat amount from the member team's pricing group
$tierAmount = $this->resolveReturnSubscriptionTierAmount($memberTeam, $totalAccounts);
$billableTeam->tab(
"Return Subscription | {$memberTeam->name} | {$totalAccounts} accounts",
$tierAmount,
[
'subscription' => $subscription->stripe_id,
'quantity' => 1,
'period' => [
'start' => now()->addMonthNoOverflow()->startOfMonth()->timestamp,
'end' => now()->addMonthNoOverflow()->endOfMonth()->addDay()->timestamp,
],
]
);
Because invoice items are additive and cannot be swapped the way subscription items can, reconciliation is delete-then-recreate: page the parent's pending items, drop the ones belonging to this member, re-tab. That makes the monthly pass idempotent, and it is the same pattern the fee and adjustment engines use.
Outcome
A member company's rate is now its own. The parent still receives one invoice; each member sees a filtered view of it, accumulated from the lines that carry its name. An admin-facing audit view rebuilds the expected item set and diffs it against live Stripe so drift is visible rather than discovered.
The weak point is that partition key. Team identity rides in the invoice-item description string rather than Stripe metadata, which means every filter, credit, and delete matches on a name — and renaming a member company between reconciles orphans the lines written under the old one. Metadata is where that belongs, and it is the first thing I would change.
Supper Club* · Kirschbaum
The Map Is The App
Finding somewhere to eat, on the word of people you actually know
I build the mobile front end and the endpoints behind it: the restaurant detail sheet and its family of sub-sheets, likes and reviews, and a mode that turns someone you follow into a map of their saved places.
- React Native
- Expo
- Laravel
- Postgres
Browse
Saved, liked, and recommended places are pin layers on one map rather than separate screens.
Open
A detail sheet at two detents carries photos, hours, reviews, and likes without burying the one action you came for.
Follow
Follow someone and the same map will re-point at their saved places, guarded server-side by an accepted follow.
- Detail sheet redesign
- Routed sub-sheets
- Reanimated sticky header
- Likes & reviews
- Friend map mode
- Feature flags
- Haptics
- Safe-area handling
Under the hood
Both of these are mobile problems that do not exist on the web: a panel that has to hold everything and stay draggable, and a map that has to become somebody else's without becoming a second screen.
One sheet, and everything has to be in it
React Native · Reanimated · routed sheets
Photos, hours, reviews, likes, and a Go button — in a panel the user is also dragging.
45% ↔ 100%
Read
Close
One sheet, and everything has to be in it
Photos, hours, reviews, likes, and a Go button — in a panel the user is also dragging.
Problem
Tapping a pin opens a sheet at 45% of the screen; drag it and it goes to full height. Into that has to fit a photo grid, the name, whether it is open right now, a stat row, a recent review, a details block, and a primary action — while the sheet itself is a gesture target and the map stays visible behind it, since this is the one sheet in the app that deliberately does not dim its backdrop.
The obvious build fails in a specific way. Put everything in one scroll view and the action you came for scrolls off the top; nest a horizontal carousel inside a vertical scroll inside a draggable sheet and the gestures fight each other. You get a panel that technically contains the information and practically buries it.
Approach
Three decisions, each of which is really the same decision — take things out of the scroll view.
The action pill is not in the sheet. Like, save, share, and Go render through the sheet host's own footer slot, so the pill is a sibling of the scroll view rather than a child of it. It stays pinned across both detents instead of riding the content's transform when the sheet expands. The cost is that a footer lives outside the sheet screen's React tree, so it cannot reach screen context — the state it needs comes through an external store, and dismissing from a footer needs a different hook than dismissing from inside. Content reserves bottom padding so the last row never hides underneath it.
The sticky header slides, and never fades. It is driven by a
scroll handler running as a worklet on the UI thread, and it animates
translateY only. That is not a style preference: the header is a
glass material view, and any alpha below 1 stops it sampling its backdrop, so a
fade renders it fully transparent rather than faint. Sliding is the only way to
hide it and keep the material alive. The offset it reveals at is measured from
the title's own layout rather than hardcoded, so a name that wraps to two lines
still hands off cleanly.
Heavy things get their own route. Reviews, the who-liked list, a single full review, reserve providers, directions apps, and the two share flows are each a separate sheet reached through one navigation hook. Nothing expands inline. Nested scrolling is limited to two horizontal strips, and the photo grid is a static layout with a full-screen viewer on tap rather than a pager, so there is no carousel competing with the sheet's own drag.
Outcome
The sheet reads as one surface with a fixed action bar, and every row that has no data — including its divider — removes itself, so a thin listing collapses instead of showing empty labels. The tappable open/closed pill does two things at once: expands the weekly hours and scrolls to them, so "when do they close?" is one tap.
Worth saying plainly: I have no before-and-after numbers for this. Nothing was instrumented and no usability testing ran. The claim is that the structure is right — the primary action cannot scroll away, and no two gesture recognizers are arguing — not that a metric moved.
Your friend's map, on your map
Laravel · React context · unit-tested camera
Viewing someone else's saved places should not mean building a second map.
mode, not screen
Read
Close
Your friend's map, on your map
Viewing someone else's saved places should not mean building a second map.
Problem
Follow someone and you should be able to see where they eat. The tempting build is a new screen: fetch their places, render a map, done. Then you own two maps — two sets of pin styles, two tap handlers, two de-duplication rules — and they drift apart the first time one of them gets a fix.
There is a second problem that only shows up in a shared feed. Someone's recent activity merges two separate review tables, and both use ordinary auto-increment integer keys on independent sequences. Row 7 in one and row 7 in the other are not the same thing, but as ids in a merged list they are indistinguishable.
Approach
Friend viewing is a mode on the map that already exists. A context holds either the user being viewed or nothing, and one derived boolean gates the overlay sources: the two non-liked layers return empty, and the liked layer re-points at the friend's places. Everything downstream — pin styling, tap handling, overlay de-duplication — is untouched, because as far as it is concerned nothing changed.
The server is the gate. One endpoint returns a user's liked places, and one
abort_unless allows it only if you are that user or you already
follow them — an accepted follow, not a pending request, which a test
covers specifically. I want to describe that precisely rather than flatter it:
it is one-directional, they need not follow you back, and it exposes their entire
liked set. The client also hides the button when it should, but that is
affordance, not enforcement. The server check is the only real one.
The camera turned out to be the fiddly part, so it became a pure function with its own tests. Fitting to a bounding box is fine until someone has liked exactly one place — then the box has zero area and the map SDK obligingly zooms to a blank view. The function returns padded bounds when there is an area and a centre-plus-fixed-zoom when there is not, with cases for no data, one pin, several pins at identical coordinates, and non-point geometry. The fly-to also fires at most once per entry, tracked by a ref, so a background refetch does not yank the map back from wherever the user panned it.
For the id collision, the feed now emits ids prefixed by their source table instead of the raw row id.
Outcome
One map, two modes, and a fix that fails safe: if the friend's places fail to load, the map alerts and drops out of friend mode rather than sitting there empty. Leaving is a one-shot handoff — the viewed user is stashed and consumed once by the friends list, which reopens their profile.
On the ids, the honest scope is small and I would rather say so than inflate it. That value is consumed in exactly one place, as a React key, so the failure mode was duplicate sibling keys — not a tap opening the wrong record. There is no ticket, no log, and no report behind it. The accurate statement is that a collision was possible and is now impossible, found by reading the merge rather than by anything going wrong.
TrailTag* · Kirschbaum
Know The Second They're Found
A pet ID tag anyone can scan with the phone already in their pocket
I work on the commerce side rather than the tag: affiliate attribution and partner postbacks, the embeddable lead-capture forms that run on partner sites, Klaviyo lifecycle events, and the product-detail purchase flow.
- Laravel
- Livewire
- Filament
Tag
Clip the waterproof, biteproof tag to any collar. It works the moment it is on.
Scan
Whoever finds your pet scans the tag with any smartphone (no app, no account needed).
Reunite
You are alerted the second the tag is scanned, with the scan location attached.
- 76k+ pet parents protected
- 4.9★ average rating
- Waterproof
- Biteproof
- No app required
- Works with any smartphone
- Instant scan alerts
- Location on every scan
Under the hood
Every affiliate network wants the same three numbers back, and no two of them agree on what to call them. Getting that wrong does not throw — it records a sale against nobody.
Partner postbacks nobody agrees on the shape of
Queued listener · per-affiliate URL templates
Every affiliate network wants the same numbers back under a different name.
{macro} → value
Read
Close
Partner postbacks nobody agrees on the shape of
Every affiliate network wants the same numbers back under a different name.
Problem
A lot of TrailTag's traffic arrives through affiliate networks, and each one wants to be told when a click turns into a sale. That notification is a postback: an HTTP call to a URL the network gives you, carrying the transaction, the amount, and whichever sub-IDs they used to track the click.
The catch is that no two networks name those the same way. One wants
{transaction_id}, the next wants {clickid}, a third
wants both plus a {sub_source} it calls sub1 on the way
in. Hard-coding a partner's shape means the next partner is a deploy.
Approach
The postback URL is stored per affiliate, macros and all, and gets filled at send
time from the query string the purchase actually arrived on. The send runs off a
queued listener on ProductPurchaseCreated, so a slow or unreachable
partner never sits in front of a customer's checkout.
$queryParams = [];
parse_str($productPurchase->purchaseLog->url_query, $queryParams);
// Need to update keys to match the expected macros
$postbackUrl = $this->replaceMacros($affiliate->postback_url, [
'transaction_id' => data_get($queryParams, 'transaction_id'),
'amount' => $transaction->amount,
'source' => data_get($queryParams, 'source'),
'sub_source' => data_get($queryParams, 'sub1'),
]);
$response = Http::post($postbackUrl);
Log::info('Partner postback sent', [
'transaction_id' => data_get($queryParams, 'transaction_id'),
'postback_url' => $postbackUrl,
'response' => $response->json(),
]);
return $response->successful();
The substitution itself is the small decision worth pointing at. An unrecognised macro is returned as it was found rather than replaced with an empty string.
private function replaceMacros(string $url, array $values): string
{
return preg_replace_callback('/\{([^}]+)\}/', function ($matches) use ($values) {
$macro = $matches[1];
return $values[$macro] ?? $matches[0];
}, $url);
}
A partner receiving a literal {clickid} in their postback will tell
you about it within the hour. A partner receiving clickid= will
quietly record a sale against nothing, and you will find out at reconciliation.
Leaving the macro intact turns a silent mismatch into an obvious one.
Outcome
Onboarding a network is a row, not a release. Whatever they call their fields goes into the URL template, and the fill happens against the same parsed query string every purchase already stores.
Attribution that was already in the database
Backfill command · idempotent by shape
The orders were right. The credit on them was missing, and the answer was sitting in a column we already had.
--dry-run first
Read
Close
Attribution that was already in the database
The orders were right. The credit on them was missing, and the answer was sitting in a column we already had.
Problem
Product purchases were landing with a null affiliate_id. The money was
correct and the customer had their tag, so nothing looked broken from the outside.
An affiliate who had genuinely earned the sale just did not get credited for it.
The useful part: the purchase log stores the raw url_query the visitor
arrived on, and the affiliate's UUID was sitting in it the whole time. This was not
lost data. It was unjoined data.
Approach
A command that finds exactly the purchases in that state, re-parses the query it came in on, and relinks them.
protected $signature = 'affiliates:fix-tracking {--dry-run} {--since=}';
// Purchases whose log still carries the query string it arrived on,
// but whose affiliate_id never got written.
$query = PurchaseLog::query()
->with('purchasable')
->whereNotNull('url_query')
->where('purchasable_type', 'billing_order')
->whereExists(function ($query) {
$query->from('product_purchases')
->whereColumn('product_purchases.id', 'purchase_logs.purchasable_id')
->whereNull('affiliate_id');
});
// ...
$affiliateId = $queryParams['affiliate'] ?? $queryParams['affiliate_id'] ?? null;
Two flags carry most of the value. --since keeps a repair scoped to
the window you actually broke, and --dry-run runs the whole thing
inside a transaction it then rolls back, so the rehearsal reports the same counts
the real run will. The whereNull('affiliate_id') in the query is what
makes it safe to run twice: a purchase that already has an affiliate is not a
candidate, so a second pass finds nothing to do.
Outcome
Credit restored without touching an order or a payment, and a repair path that can be rehearsed before it is trusted. The tracking cookie middleware and the extra affiliate columns went in alongside it, so new purchases stopped needing the command in the first place.
Blue Horse Entries · Birdboar
Discover & Ride
Show entry, stabling, and every fee in one checkout — on money the platform never touches
Lead engineer, and the author of most of the codebase. Payments on Stripe Connect, the wizard engine all three flows run on, and the fee and capacity model are mine. I came back in 2024 for a season of hardening on top of that.
- Laravel
- Livewire
- Stripe
- shows hosted
- 1,200+
- entries processed
- 48k
- Breed
- Warmblood
- Height
- 16.2 hh
- USEF #
- 532 4410
Find and enter clinics and competitions with search filters, personalized event suggestions, and fees that calculate themselves.
Every child's entries in one account. Collaborate with teen riders, and get through a full entry even if this is your first show season.
An event builder for rapid setup, digital roster management, delegation for staff, and deadline alerts that chase paperwork for you.
“An accessible, inclusive equestrian community that empowers you to be courageous both in and out of the saddle.”
Under the hood
A show entry looks like a form. Underneath it is a two-sided marketplace where the platform never holds the money, a checkout that changes shape per event, and a stall count that has to stay honest while forty people are shopping.
Marketplace payments on Stripe Connect
Stripe Connect · destination charges
Every entry fee runs through the platform, and the platform never holds a cent of it.
PAID is terminal
Read
Close
Marketplace payments on Stripe Connect
Every entry fee runs through the platform, and the platform never holds a cent of it.
Problem
Organizers run their own shows and keep their own money. Blue Horse takes a cut and never sits on the balance, which means every charge has to execute on the organizer's connected Stripe account rather than the platform's.
That much is in the docs. What is not obvious until you hit it: a
PaymentMethod created on the platform account cannot be used on a
connected account. So the rider saves a card at the platform level, and then the
charge that needs that card lives somewhere that cannot see it.
Approach
Clone the payment method and the customer onto the connected account, then create the
intent there with the platform's cut declared as application_fee_amount.
Stripe settles the remainder to the organizer directly.
// A PaymentMethod created on the platform account cannot be used on a connected
// one, so both it and the customer are cloned onto the organizer's account first.
$paymentMethod = \Stripe\PaymentMethod::create([
'customer' => $payer?->stripe_id ?: $this->user->stripe_id,
'payment_method' => $paymentMethodId,
], [
'stripe_account' => $this->event->team->stripe_id,
]);
$customer = $stripe->customers->create([
'payment_method' => $paymentMethod->id,
'name' => $payer?->name ?: $this->user->name,
'email' => $payer?->email ?: $this->user->email,
], ['stripe_account' => $this->event->team->stripe_id]);
$intent = \Stripe\PaymentIntent::create([
'setup_future_usage' => 'off_session',
'amount' => round($this->total * 100),
'currency' => 'usd',
'customer' => $customer->id,
'payment_method' => $paymentMethod->id,
'automatic_payment_methods' => ['enabled' => 'true'],
// the platform's cut — everything else settles to the organizer
'application_fee_amount' => round($this->calculatePlatformFee() * 100),
'metadata' => ['order_id' => $this->id, 'paid_by' => $payer?->id ?: $this->user->id],
], [
'stripe_account' => $this->event->team->stripe_id,
]);
The other half is not trusting the browser. An order becomes paid when
Stripe says so, which means the webhook does the fulfilment and the checkout response
does not. That makes status the field everything else hangs off, so the behaviour lives
on the enum instead of scattering across callers.
enum OrderStatus: string
{
case DRAFT = 'draft';
case SUBMITTED = 'submitted'; //sent to stripe
case PROCESSING = 'processing';
case PAID = 'paid';
case PAYMENT_FAILED = 'payment_failed';
// ... CANCELED, APPROVED, DECLINED, ERRORED, PENDING
public function payable(): bool
{
return in_array($this, [self::DRAFT, self::DECLINED, self::ERRORED, self::PAYMENT_FAILED]);
}
public function retryable(): bool
{
return in_array($this, [self::DECLINED, self::ERRORED, self::PAYMENT_FAILED]);
}
public function retryMessage(): string
{
return match ($this) {
self::PAID => 'Your order is paid. No need to retry.',
self::DECLINED => 'Your order was declined. You may retry with a different payment method.',
// ...
};
}
}
// App\Models\Order — PAID is a one-way door.
public function updateStatus(OrderStatus $status)
{
$this->refresh();
if ($this->status === OrderStatus::PAID) {
return true;
}
$this->status = $status;
return $this->save();
}
updateStatus() refuses to move out of PAID at all. Webhooks
arrive out of order, reconciliation jobs re-run, and a rider hits retry on a stale tab.
Each of those is a chance to walk a paid order backwards, and none of them should be
able to.
Outcome
Ten states that answer their own questions through payable(),
retryable(), and editable(), so no caller has to remember
which ones are safe. Fees gross up so processing costs pass through to the buyer rather
than eating the organizer's margin, and rider identity runs through Stripe Identity
with document plus matching-selfie verification.
Fulfilment is webhook-driven, with CleanupMissedPaymentIntents and
CleanupDuplicateTransactions on a schedule for the times a webhook never
lands. “Stripe will always call us back” is a fine assumption right up
until it isn't, and the failure mode is somebody's entry silently not existing.
A wizard engine, not a fourth wizard
Livewire 2 · framework-level abstraction
Seven steps to publish a show, eight to enter one. The second flow is where you decide what you are actually building.
3 flows · 1 engine
Read
Close
A wizard engine, not a fourth wizard
Seven steps to publish a show, eight to enter one. The second flow is where you decide what you are actually building.
Problem
Publishing an event takes seven steps. Entering one takes eight, and gains or loses steps depending on what the organizer chose to collect. Both need per-step state, deep links that resume where you left off, a guard so nobody jumps into step five, and a discard prompt that knows whether there is any work to lose.
Livewire 2 shipped none of that. One flow you hand-roll. By the second you are choosing between a fourth copy of the same scaffolding and an abstraction.
Approach
WithWizardry on the container, Steppable on each step, with
Step and State as the value objects between them. Forward
navigation is gated on the previous step reporting itself complete, so a hand-edited
URL cannot skip validation.
public function allowNavigation(string $stepName)
{
$previousStep = collect($this->stepNames())
->before(fn (string $step) => $step === $stepName);
// the first step is always reachable
if ($previousStep === null) {
return true;
}
if (! array_key_exists($previousStep, $this->allStepState)) {
return false;
}
if (array_key_exists('isComplete', $this->allStepState[$previousStep])) {
return $this->allStepState[$previousStep]['isComplete'];
}
return false;
}
Steps are classes, so a flow is just a list, and a conditional flow is a list built at runtime. That is what earns the abstraction: registration cannot be a fixed sequence, because the organizer decides its shape.
// App\Http\Livewire\Events\Wizard\EventWizardIndex — a fixed flow
public function steps(): array
{
$steps = [
EventBasicInfoStep::class,
EventBrandingStep::class,
EventPeopleStep::class,
EventLevelsFeesAndExtrasStep::class,
EventSettingsStep::class,
EventDetailsStep::class,
];
if (isset($this->event)) {
$steps = array_merge($steps, [EventPublishStep::class]);
}
// ...
}
// App\Http\Livewire\Events\Register\RegisterWizardIndex — assembled per event
public function steps(): array
{
if (! $this->isAudit) {
$steps = [
0 => RegisterRiderSelectionStep::class,
1 => RegisterHorseSelectionStep::class,
3 => RegisterRidesSelectionStep::class,
];
// only asked for when the organizer left the horse experience field visible
if (isset($this->event->included_fields['horse']) && collect(...)->contains(...)) {
$steps = array_merge($steps, [RegisterExperienceStep::class]);
}
// ...
}
}
Outcome
Three flows on one engine (event creation, registration checkout, and horse setup)
with per-step isolated state, deep-linkable resumption, and
canDiscardUntil() so the discard warning only fires once there is
something worth warning about. New steps are a class and a line in an array.
Layered fees and honest inventory
9 fee types · derived capacity
Nine fee types resolved against event, entry, and organizer config, plus a stall count abandoned carts cannot hold hostage.
submitted only
Read
Close
Layered fees and honest inventory
Nine fee types resolved against event, entry, and organizer config, plus a stall count abandoned carts cannot hold hostage.
Problem
An entry is not one price. It is an entry fee, possibly a late fee evaluated against the organizer's own timezone, show fees that apply only when the entry includes a matching discipline, a sanctioning-body surcharge priced by counting how many people on the entry lack a membership, stabling billed per day and split between standard and tack stalls, audits, extras, and the platform's cut, flat or percentage, per organizer.
Then stabling has capacity. Capacity is where this kind of system quietly loses money.
Approach
Nine fee types, each resolving against event, entry, and org config. The pricing detail worth pulling out is the gross-up. Processing costs are added to what the buyer pays rather than subtracted from what the organizer receives, so the advertised price is the price they actually get.
protected function processingFee(): Attribute
{
return Attribute::make(
get: fn () => round(
($this->getSubtotal() + $this->getTaxes() + $this->calculatePlatformFee() + 0.30) / 0.971
- $this->getSubtotal() - $this->getTaxes() - $this->calculatePlatformFee(),
2
)
);
}
For capacity the real decision is what counts as taken. Counting carts means one rider with a tab open holds a stall nobody else can buy. So capacity is never stored. It is derived, and it derives from submitted entries only.
public function entries()
{
return $this->belongsToMany(Entry::class)
->using(EntryFee::class)
->withPivot('amount', 'quantity')
->whereSubmitted(); // <- a cart holds nothing
}
protected function ordered(): Attribute
{
return Attribute::make(get: function () {
return str($this->type)->contains('stabling')
? $this->entries->count()
: $this->entries->sum('quantity');
});
}
protected function remaining(): Attribute
{
return Attribute::make(
get: fn () => isset($this->capacity) ? $this->capacity - $this->ordered : null
);
}
protected function isSoldOut(): Attribute
{
return Attribute::make(
get: fn () => isset($this->capacity) ? $this->remaining <= 0 : false
);
}
That is a trade, not a free win: two riders can both be looking at the last stall and one of them loses at checkout. For a horse show that is the right side to fail on. It is also exactly the kind of decision that regresses silently, so it is pinned.
test('test only submitted entries impact remaining/sold out status', function () {
Auth::login($user = User::factory()->create());
$fee = Fee::factory()->state(['capacity' => 1, 'type' => 'standard-stabling'])->create();
$entry = Entry::factory()->state(['user_id' => $user->id])->create();
$entry->fees()->sync([$fee->id]);
expect($fee->remaining)->toBe(1);
expect($fee->isSoldOut)->toBeFalse();
$entry->update(['status' => 'submitted']);
expect($fee->refresh()->remaining)->toBe(0);
expect($fee->refresh()->isSoldOut)->toBeTrue();
});
Outcome
Stall counts that reflect money actually committed, and a fee model that keeps the organizer's advertised price whole.
Coverage on this build went where the money went: fee capacity, cart management, exports, with Bugsnag and the reconciliation commands carrying the rest. I was the only engineer on it for most of that stretch, against a live seasonal deadline, so the gaps were a trade I made deliberately rather than one I would defend as ideal.
Race Passport · Birdboar
Live. Venture. Explore.
Find your next foot race and enter it on the spot
Lead engineer. I wrote roughly three quarters of the codebase, from race listings through registration and payment.
- Laravel
- Livewire
- Alpine
- races listed
- 300+
- runners registered
- 25k
Route::post('/races/{race}/register', function (Race $race) {
$entry = $race->entries()->create(
request()->validate([
'runner_name' => ['required', 'string'],
'distance' => ['required', 'in:5k,10k,half'],
])
);
return Payment::for($entry)->checkout();
});
“Taking you where you want to go.”
BlueCheck · Birdboar
Age & Identity, Verified
Test coverage and API authentication for an identity verification platform
A two-month engagement inside a large existing codebase, so my part is a slice rather than a build. February went to test coverage across the three services, including the false-pass bug below. March went to authentication: Jetstream across four apps, Spatie roles, and moving the API onto Sanctum tokens issued to domains.
- PHPUnit
- Sanctum
- Jetstream
- services covered
- 3
- suites written or expanded
- 11
public function testRelaxedNameMatchStillChecksTheFirstName()
{
$endUser = EndUser::factory()->create([
'first_name' => 'Jane',
'last_name' => 'Doe',
]);
$this->assertFalse(
$endUser->nameSimilarTo('John', 'Doe', onlyNeedFirstTwoCharactersOfFirstName: true)
);
}
Verifying
Tests for BlueCheck's Person Data Search service, which verifies a user's age from their PII. Age laws differ by jurisdiction and keep changing, and this suite is how the team knows their checks still hold.
Processing
Coverage for the manual and batch verification flows on BlueCheck's Merchant platform.
Integrating
And tests for the APIs and embeds that bring BlueCheck verification to Shopify, WooCommerce, and BigCommerce storefronts.
- 15M+ individuals verified
- 160+ countries supported
- 4.9★ across 3,900+ Shopify reviews
Under the hood
Two months, two jobs. The first found a bug that let the wrong person pass a check. The second answered a question the framework has an opinion about and the product did not.
The comparison that compared nothing
PHPUnit · shared/Models/EndUser
A relaxed name check meant to forgive Kate for Katherine was letting through John for Jane.
last == last
Read
Close
The comparison that compared nothing
A relaxed name check meant to forgive Kate for Katherine was letting through John for Jane.
Problem
Identity checks have to tolerate the difference between the name on someone's licence
and the name they typed into a checkout. Katherine buys as Kate. Michael is Mike. So
nameSimilarTo() takes a flag,
$onlyNeedFirstTwoCharactersOfFirstName, that relaxes the match: the last
name still has to be exact, but the first name only needs its opening two characters.
That is a reasonable rule. It was not the rule the code implemented.
Approach
Here is the relaxed branch as it stood.
public function nameSimilarTo($firstName, $lastName, $onlyNeedFirstTwoCharactersOfFirstName = false)
{
// ...exact match on the full name first, then the relaxed path:
if ($onlyNeedFirstTwoCharactersOfFirstName) {
if (! strcasecmp($normalizedLastName, $thisNormalizedLastName) &&
! strcasecmp(mb_substr($normalizedLastName, 0, 2), mb_substr($thisNormalizedLastName, 0, 2))) {
return true;
}
}
return false;
}
Read the two conditions together. The first says the last names must match exactly. The second says the first two characters of the last names must match, which is already guaranteed by the first, because identical strings have identical prefixes. The second clause can never fail once the first has passed.
So the branch reduced to "the last names are the same," and the first name was never examined at all. Every name in that relaxed path was compared against itself. A Jane Doe could clear a check written for a John Doe, and the method would report a similar name, because it never looked at either first name.
The fix is one word in each argument, which is what makes it the kind of bug that survives review. It reads correctly at a glance.
if ($onlyNeedFirstTwoCharactersOfFirstName) { // may be also able to attempt a match this way
if (
!strcasecmp($normalizedLastName, $thisNormalizedLastName) &&
!strcasecmp(mb_substr($normalizedFirstName, 0, 2), mb_substr($thisNormalizedFirstName, 0, 2))
) {
return true;
}
}
Outcome
A false pass in an age and identity check, closed. This one came out of expanding
VerificationServiceTest rather than from a bug report, which is the honest
argument for coverage on this kind of product: nobody files a ticket when verification
says yes. The failure is silent by construction, and the only thing that catches it is a
test that asserts the negative case.
The rest of that stretch went the same way. Eleven suites written or expanded across the three services, plus fixes the tests turned up on the way, like a notification reason that needed encoding before it went out over the webhook.
API tokens that belong to a domain
Sanctum · Jetstream · 4-app monorepo
The thing calling the verification API is a storefront, and a storefront is not a person.
tokenable != User
Read
Close
API tokens that belong to a domain
The thing calling the verification API is a storefront, and a storefront is not a person.
Problem
BlueCheck's verification API is called by merchants' storefronts on Shopify, WooCommerce, and BigCommerce. The caller is a domain. It has no inbox, it never logs in, and it does not stop being a caller when an employee leaves.
Laravel's token tooling is written for users. Jetstream hangs API tokens off the authenticated account, and the surrounding docs assume a person clicking a button. The platform meanwhile had its own hand-rolled domain tokens, already in production and already in customers' storefront configuration, which is the sort of credential you cannot simply invalidate on a Tuesday.
Approach
Sanctum's tokenable relationship is polymorphic, which the User-shaped
documentation tends to bury. Nothing requires the owner of a token to be a person, so
the domain became the token holder directly.
use Laravel\Sanctum\HasApiTokens;
class Domain extends Model
{
use HasFactory;
use SoftDeletes;
use LogsModelActivity;
use HasApiTokens;
// ...
}
With that in place, a domain gets Sanctum's abilities, hashing, and revocation for free,
and the verify API's routes move behind auth:sanctum like any other guarded
endpoint. Configuration lives in a shared service provider rather than four copies,
because this is a monorepo where account, admin, merchant, and verify are separate
Laravel apps over one shared/ directory.
Then the part that actually carried the risk: the tokens already out there had to end up in the new system without any merchant editing their storefront.
class DomainTokenAPITransfer extends Command
{
protected $signature = 'tokenize:domain';
protected $description = 'Allows domain tokens to use Jetstream API token system';
// ...
}
A migration for live credentials is a different animal from a schema migration. Getting it wrong does not throw, it just quietly starts refusing traffic from stores that are legally obliged to check ages before they sell.
Outcome
Domains hold their own API tokens, issued and revoked through the same Jetstream screens
everything else uses, on Sanctum rather than a bespoke scheme. Roles and permissions went
onto spatie/laravel-permission in the same stretch, and the Jetstream models
are overridden from shared/ so all four apps authenticate against one set.
Two months inside a codebase with ten thousand commits and a dozen contributors before I arrived. The pieces above are mine. Almost everything around them is not.
Pinwheel · Fayette County Public Schools
Approve & Progress
Personnel and budget requests, on approval chains the district builds for itself
Solo build, start to finish. I wrote all but a handful of the commits in this one, from the field builder to the approval engine.
- Laravel
- Livewire
- Filament
- field types
- 11
- requests routed
- $100k+
- Principal · A. Whitfield Approved · Mar 4
- Budget office · D. Reyes Approved · Mar 5
- Finance director · Unassigned Pending
Submit
A department head drafts a personnel or budget request.
Review
Designated approvers weigh in at each tier in order.
Approve
The request clears the chain and is logged for the record.
Under the hood
Calling this an approval workflow undersells the part that took the time. The district keeps inventing new kinds of request, and none of them were allowed to need a deploy.
A form builder that keeps its types
Enum registry · typed value columns
An admin invents a new question on Tuesday. Finance still wants to sum the answers on Friday.
11 types · 0 blobs
Read
Close
A form builder that keeps its types
An admin invents a new question on Tuesday. Finance still wants to sum the answers on Friday.
Problem
Budget requests and personnel requests do not ask the same questions, and neither asks the same questions this year as last. Whoever runs the process needs to add a field without filing a ticket.
There are two easy answers and both are bad. A migration per request type makes every new question a deploy. One JSON column per request makes every question free and every report expensive, because a budget total stored as text is not a number you can add up in the database.
Approach
One enum is the registry. Each case declares the PHP class that owns the type, the Blade component that renders it, and the icon the builder shows, so adding a field type is one case rather than an edit in four places that drift apart.
enum FieldType: string
{
case CHECKBOX = 'checkbox';
case DATE = 'date';
case DECIMAL = 'decimal';
case LOOKUP = 'lookup';
case LINEITEM = 'line-item';
// ...
public function details(): object
{
return match ($this) {
self::DECIMAL => (object) [
'name' => 'Decimal',
'value' => 'decimal',
'type' => 'float',
'class' => 'App\\BasicTypes\\FloatType',
'component' => 'input.number',
'icon' => 'receipt-percent',
],
self::LOOKUP => (object) [
'name' => 'Lookup Relationship',
'value' => 'lookup',
'type' => 'lookup',
'class' => 'App\\BasicTypes\\LookupType',
'component' => 'input.select',
'icon' => 'clock',
],
// ...
};
}
}
The part that keeps reporting cheap is Mappable. A type declares which real
column its value lands in, so a decimal is stored as a float and a lookup is stored as a
foreign key.
class LookupType extends FieldBasicType implements Mappable
{
protected static function booted()
{
static::addGlobalScope('name', function (Builder $builder) {
$builder->where('name', 'lookup');
});
}
public static function mapToValueColumn(): string
{
return 'integer';
}
}
Values stay typed all the way down, which means a total is still a SUM and a
date range is still an index scan. The flexibility is in the definition, not in the
storage.
Outcome
Eleven field types, from checkbox to line item, composable into any request type without a
migration. Each field also carries a FieldPermission, so who may see it and
who may edit it is part of the same definition rather than a second rule kept somewhere
else and forgotten.
Denial as arithmetic
Quorum steps · conditions · designees
Nobody clicks deny. The step just runs out of people who could still say yes.
pending + approved < required
Read
Close
Denial as arithmetic
Nobody clicks deny. The step just runs out of people who could still say yes.
Problem
"Three tiers" is what the org chart says. What the process actually does is subtler: a step can need two of its five approvers, a step might not apply to this request at all, and the chief is on leave so somebody signs for them.
The one that shapes the code is denial. If a step needs two approvals and two of three people have already declined, the request is dead and should say so, without waiting for the third person to come back from vacation and click a button.
Approach
So status is not set, it is computed, from a count of what is approved against a count of what could still arrive.
$required = $this->currentStep->required;
$approved = $this->approvals()->where('approval_step_id', $this->approval_step_id)->whereApproved()->count();
$pending = $this->approvals()->where('approval_step_id', $this->approval_step_id)->wherePending()->count();
if ($approved >= $required) {
if ($nextStep = $this->nextStep) {
$this->update(['approval_step_id' => $nextStep->id]);
} else {
$this->update(['status' => 'approved']);
}
} elseif ($pending + $approved < $required) {
$this->update(['status' => 'denied']);
}
That second branch is the whole idea. pending + approved < required reads
as "even if every undecided person says yes, this step cannot reach quorum," and that is
the moment the request is denied. Nobody has to make the call, because the arithmetic
already made it.
Standing in for an absent approver is the other half, and it resolves through the delegator rather than the person acting.
// App\Models\User - the same pivot read from both ends
public function designees()
{
return $this->belongsToMany(User::class, 'user_designees', 'user_id', 'designee_id');
}
public function delegators()
{
return $this->belongsToMany(User::class, 'user_designees', 'designee_id', 'user_id');
}
// ...and the policy asks the delegator's question, not the actor's:
public function create(User $user): bool
{
return $user->ownsTeam($user->currentTeam)
|| $user->currentTeam->owner->designees->pluck('id')->contains($user->id);
}
One pivot table read from both directions gives you designees and
delegators for free, and because the policy asks whether the actor is in the
team owner's designee list, a designee inherits authority without anyone copying
permissions onto them.
Outcome
Steps carry their own quorum, and each one is Conditionable, so a step can
apply only when the request matches a rule. Fourteen rule classes back those conditions,
from the generic FieldIsGreaterThan to the district's own
PersonnelBenefitsRatio and StrategicPriorityRequired.
A step can also point at an escalation step, and an individual approval can be skipped, so the paths around the happy path are configuration rather than special cases in the controller. This one was a solo build, which is the reason I can point at the pieces I would still defend and the ones I would not.
Elevate · Fayette County Public Schools
Every Student, A Pathway
College and career readiness, tracked from the first milestone to the last
I built the student-facing half: profiles, the evidence workflow behind every milestone, teacher dashboards, cover photos and media, and the shared form and slideover components the rest of the app got built on. The Infinite Campus sync and the badge-rule engine were a teammate's.
- Laravel
- Livewire
- Alpine
Choose
A student picks a program area and the pathway underneath it, and the milestones for the term lay themselves out in order.
Evidence
Each milestone takes the artifact that proves it (a certificate, a reflection, hours logged) and holds it for a counselor to review.
Recognize
Overnight jobs check every rule and award the badges that have been earned, so nobody has to go looking for them.
- Google SSO
- Infinite Campus imports
- Goals & reflections
- Surveys
- Announcements
- Counselor analytics
Dispatch · Fayette County Public Schools
Every Call, On The Record
A dispatch console that reads the radio traffic as it is typed
Lead on a two-person build. The call and code models, the call widget, and the assignment flow are mine.
- Laravel
- Livewire
- Jetstream
Log
A dispatcher opens a call and types into it. The message is the record, so there is no second form to fill in afterward.
Parse
Each message is checked against the dispatcher's own codes and shortcuts, so a reference in plain text becomes structured data on the call.
Dispatch
Officers are assigned from inside the call, gated by on-duty status, and a wrong unit can be taken back without unwinding the log.
Under the hood
A dispatcher on a live call is not going to stop and fill in a form, and the record still has to be structured afterward. That tension is the whole build.
A chat box that is quietly a command line
Livewire · whereIn over the message
Dispatchers type the way they talk. The call still ends up with a code, a priority, and units on it.
1 query · every word
Read
Close
A chat box that is quietly a command line
Dispatchers type the way they talk. The call still ends up with a code, a priority, and units on it.
Problem
Radio traffic arrives fast and in whatever order it happened. A dispatcher taking a call is listening, typing, and watching who is free, all at once. Any interface that asks them to leave the message box and pick a code from a dropdown loses to a legal pad.
But the call is also a record. Somebody will read it back later and needs to know which code it was, when the priority changed, and who was sent.
Approach
Every message gets checked against the department's own code table on the way in. The
lookup is the message split on spaces, handed to the database as a single
whereIn, so matching a hundred codes against a sentence is one query rather
than a loop.
public function parseForCode($info)
{
if ($foundCode = Code::query()
->whereIn('reference', explode(' ', $info))
->first()
) {
$this->call->code_id = $foundCode->id;
$this->call->save();
$this->handleUpdatedCode($foundCode);
}
$this->call->refresh();
}
Escalation then belongs to the code rather than to the parser. A code carries a
default_priority, and if it has one the call takes it, which means the
department can decide that 10-50 is a High without anyone shipping a
match statement.
public function handleUpdatedCode($code)
{
activity('code')
->causedBy(Auth::user())
->performedOn($this->call)
->log('Code ' . $code->code . ': ' . $code->description);
if ($code->default_priority) {
$this->call->priority = $code->default_priority;
$this->call->save();
activity('priority')
->causedBy(Auth::user())
->performedOn($this->call)
->log("Escalated to " . $code->default_priority . " priority.");
}
$this->call->refresh();
}
Both facts are written to the activity log as they happen, which is the detail that makes the record trustworthy later. The log is not reconstructed from the call's current state, it is what was true at each moment.
On top of the codes sits a very small vocabulary, learned once and then muscle memory.
// "U7" anywhere in the subject line puts unit 7 on the call
preg_match("/U(\d+)/", $this->call->subject, $matchedUnit);
if (count($matchedUnit) > 1) {
$user = User::where('unit_number', $matchedUnit[1])->first();
$this->dispatch($user);
}
// ...and "du 4 7 12" in a message dispatches all three
} elseif (str_contains($this->note, 'du ')) {
$units = str_replace('du ', '', $this->note);
foreach (explode(' ', $units) as $unitId) {
try {
$user = User::where('unit_number', $unitId)->firstOrFail();
$this->dispatch($user);
} catch (\Throwable $th) {
// No User Found.
}
}
return true;
}
A missing unit number is swallowed on purpose. A dispatcher who fat-fingers a unit while typing a real message should get the message through and the wrong unit ignored, not an error page in the middle of a call.
Outcome
Dispatchers keep typing prose and the structure falls out of it. Codes attach, priorities escalate, units get dispatched, and each of those writes its own log line.
The call's timeline is then a merge of two streams, the comments people wrote and the activities the system recorded, sorted back together into one readable history. Reading a call back gives you what was said and what the software did about it in the order both happened, which is the only version that helps at review time.
Planbook · Fayette County Public Schools
The Week, Delivered
Lesson planning that keeps preschool families in the loop without asking teachers for a second draft
Primary developer. The weekly digest, the subscriber model, and the teacher-run resend are mine.
- Laravel
- Livewire
Plan
A teacher drafts the week inside a unit, one lesson plan per day, in the same place they already keep their scope and sequence.
Tag
Standards attach to the plan itself, so coverage is a query rather than a spreadsheet someone rebuilds each spring.
Send
Sunday at eight the week goes out as one email: teacher on the To line, parents blind-copied, resendable by whoever wrote it.