Skip to content
All work
LiveJun 2023 — Jul 2026· saas

Aurora

Product authenticity platform: manufacturers and shops get verified, every unit gets a serial number and a QR code, and buyers can check the chain themselves.

Role
Sole engineer — undergraduate thesis project
Team size
1 person
Timeline
Jun 2023 — Jul 2026
Status
Live

The problem

A counterfeit looks exactly like the real thing right up to the moment it fails. The usual answer — a hologram sticker, a phone number to call — asks the buyer to trust a second thing that is equally easy to fake. Aurora moves the proof off the packaging and onto a chain of custody the buyer can verify from their own phone.

Moving the proof off the sticker

Anti-counterfeiting on packaging has a structural problem: whatever you print on the box, a counterfeiter can print too. Holograms, scratch panels, serial stickers — all of them are copied within months, and the buyer has no way to tell a copied proof from a real one because both are just ink.

Aurora starts from the other end. The manufacturer is verified by a human before they can list anything. Each unit they create gets a serial number and a QR code that resolves to a page on our side. When a shop buys the unit, ownership transfers and the unit is frozen. When a customer scans the code, they are not reading the packaging — they are reading a record that names who made it, who sold it, and when.

The rules are the product

Almost every interesting decision in this codebase is a restriction rather than a feature. Sold products cannot be edited. QR codes cannot be regenerated. Ownership only moves forward. None of those are hard to implement — the work is noticing that each one is load-bearing, because a chain of custody with an escape hatch is not a chain of custody.

app/Models/Product.php
protected function serialNumber(): Attribute
{
    return Attribute::make(
        set: fn () => $this->generateSerialNumber(),
    );
}

private function generateSerialNumber(): ?string
{
    $serial_number = fake()->unique()->numerify('SN-########');

    while ($this->where('serial_number', $serial_number)->exists()) {
        $serial_number = fake()->unique()->numerify('SN-########');
    }

    return $serial_number;
}

The setter ignores whatever it is handed. That is intentional: a serial number is not something a caller gets to propose, and the cheapest way to enforce that is to make the attribute incapable of accepting an outside value in the first place.

Status

Built as my undergraduate thesis at Northern University Bangladesh, and shipped rather than left as a report. Roles, approval, products, ownership transfer and QR verification all work end to end. Active development has stopped — it does what it was built to do.

Architecture decisions

A single Profile model carries both manufacturers and shops, with nullable role-specific fields

Why · The two roles share most of what matters — approval state, QR code, address, ownership of products — and differ mostly in which fields are filled in. One table means one approval workflow and one policy to reason about.

Trade-off · A wide table with a lot of nullable columns, and validation that has to branch on role rather than lean on the schema.

A profile's QR code is generated once, when an admin approves it, and can never be changed

Why · The QR code is the proof. If it can be regenerated, it proves nothing — an attacker who compromises an account could mint a fresh one. It is removed from the fillable attributes entirely and written through a separate admin-only path.

Trade-off · No recovery if a code is ever issued wrongly. That is the correct trade for a credential.

Sold products are immutable — once a shop sells a unit to a customer it cannot be edited, deleted or sold again

Why · A transfer of ownership is a historical fact. If the seller can edit a unit after the sale, the record stops being evidence and becomes a claim.

Trade-off · Genuine mistakes need an admin to intervene. Worth it — the alternative is a chain of custody nobody has a reason to believe.

Serial numbers are generated with a uniqueness re-roll rather than a sequence

Why · Sequential serials leak production volume to anyone holding two units. A random serial checked against the table gives uniqueness without the leak.

Trade-off · A read per generation, and a loop that has to be bounded as the table grows.

What broke, and how it was fixed

Ownership started out keyed to `manufacturer_id`, which quietly assumed a product only ever belongs to the party that made it.

Migrated every reference to `profile_id`. Once ownership pointed at a profile rather than a role, a manufacturer selling to a shop and a shop selling to a customer became the same operation applied twice, instead of two special cases.

Manufacturers add units in batches, and adding them one form at a time was unusable at that volume.

Added bulk creation — one form, a count field — and duplication of an existing unit, with a fresh serial number and QR URL minted per copy so no two units share an identity.

Admins needed to know about profile updates without living in the notifications panel.

Database notifications for the in-app panel, plus a scheduled daily digest by email. The panel is for people who are already there; the digest is for people who are not.

Keep reading

LiveAug 2021 — present

Gymscanner

Global platform for booking gym memberships, tourist passes and personal training sessions across 100+ cities.

LaravelLivewireFilament +6
LiveMay 2026 — present

Filament Fakester

A Filament plugin that fills form fields with sensible fake data in development, one click per field.

PHPLaravelFilament +3