← Back to Blog

AI-Assisted Development

Solving the Multi-Owner Gear Chaos with Claude Code

October 8, 2026

Solving the Multi-Owner Gear Chaos with Claude Code

Anyone who has packed up after a gig or a group camping trip knows the exact moment the pain sets in. Exhausted just by the sight of a pile of gear belonging to five different people, with three nearly identical mic stands, you wish for a reliable way to know that everything will go home with its proper owner.

When I went looking for an app to solve this, everything I found assumed one owner tracking their own stuff, or an organization checking rentals in and out. Nobody was handling the shared pool: one pile of gear with several owners, reconciled against each other for a specific event. So I came up with the idea for Gear Tracker. Working in my spare time, I built it in a little over three weeks, with Claude Code doing most of the typing.

How it works

Beyond the core functionality above, the app supports uploads for sharing setlists, PDF files, and whatever might be applicable to an upcoming event. There is also centralized venue maintenance, providing an easy workflow to tie an event to repeat destinations. Events are shared via the organizer sending an invitation URL to participants.

No backend of my own

The app is a React progressive web app hosted on Vercel, talking directly to Supabase, which provides the Postgres database, authentication, file storage, and realtime updates.

Architecture diagram: phones running the React PWA, served by Vercel, talk to Supabase's auth, REST API, process_scan function, and storage. Postgres row changes flow through Supabase Realtime back to every phone.

The design spec weighed two alternatives, and both lost for specific reasons:

The cost of having no backend is that the database is the backend. Every rule about access rights lives in Postgres, as an RLS policy or a database function, which turned out to be where nearly all the hard problems surfaced.

The live manifest, and the bug that made no noise

The part of the app that feels like magic is the live manifest. When one person loads their gear into an event, everyone else's screen updates without a refresh. The code is fairly minimal. The app subscribes to changes on one event's manifest rows and refetches when one arrives:

supabase
  .channel(`manifest-${eventId}`)
  .on(
    'postgres_changes',
    { event: '*', schema: 'public', table: 'item_event_manifest', filter: `event_id=eq.${eventId}` },
    onChange
  )
  .subscribe()

As first written, that code would have done nothing at all, and it wouldn't have failed either. Supabase doesn't broadcast changes from every table. It only broadcasts changes from tables listed in a Postgres "publication" called supabase_realtime, and that list starts out empty. Without the manifest table on the list, the subscription would connect cleanly but never fire, with no error or warning — just a manifest that quietly never updated.

The fix was a single line in a database migration, adding the manifest table to that list:

alter publication supabase_realtime add table item_event_manifest;

It's easy once you know, and nearly impossible to guess from the symptoms. That set the tone for the rest of the project: the dangerous bugs weren't the ones that crashed.

Scanning: the phone reads, the database decides

Each QR code encodes a URL ending with the item's ID. Inside the app's scanner, that URL is simply a carrier for the ID. But because it's a real link, pointing any phone's ordinary camera at the code opens a page showing the item's details (depending on access).

This was initially broken. The QR codes encoded a placeholder domain that was never registered, so scanning outside the app went nowhere. I didn't find out there was a problem until testing with a regular phone camera.

The scanner itself does as little as possible. It decodes the tag and hands the ID to a single Postgres function, process_scan, which makes every decision. Is the tag unknown? Is the owner not part of this event? Is it a known item that isn't on the manifest yet? Should it be marked returned? Or is there nothing to do? Keeping that logic in the database ensures that every phone gets the same answer.

A camera will decode the same QR code several times a second, so the scanner must be prevented from processing the intended scan more than once. With a simple lock in place, the first valid decode sets a flag, stops the camera, and waits for the database's answer. The lock is released only when you tap "Scan again":

const handleDecode = async (data: string) => {
  if (processingRef.current) return
  const tagId = parseTagId(data)
  if (!tagId) return
  processingRef.current = true
  scannerRef.current?.stop()
  const scanResult = await processScan(eventId, tagId, direction)
  // ...show the result, wait for "Scan again"
}

Pausing after every scan adds a moment of friction, but nothing gets double-counted, and you always see exactly what the app decided before moving on.

The process: prompting as system design

Rather than let an agent generate code freely, I ran Claude Code like a small engineering team. The core build was split into four phases:

  1. Authentication and user provisioning
  2. Item library and QR code generation
  3. Events and live manifests
  4. Load-out scanning and reconciliation

Before any code was written, each phase was planned out with a design spec and implementation plan. Once a plan was reviewed and approved, its tasks went to separate subagents, followed by task-level code reviews as well as a final review of the feature branch prior to merging. Subsequent improvements, like item photos, venues, event times, and file attachments, went through the same loop in smaller turns.

Process diagram: a design spec becomes an implementation plan, which is split into subagent tasks. Each task gets a review, then the whole branch gets a final review before merge and deploy.

By the end, the repository held 10 design specs, 11 implementation plans, and 202 commits. Claude Sonnet 5 did most of the work, with Claude Haiku 4.5 handling a handful of small, well-specified tasks (like adding a column or a data-layer function). Matching the model to the size of the task was straightforward, with documented implementation steps in place.

What the reviews caught

The reviews earned their keep. These were the most instructive catches, and most would not have shown up as an error message.

Two reasonable policies that combined into a leak. RLS policies are easy to get right one table at a time, but more difficult when used in combination. One policy allowed event members to see each other's items, which is a requirement of the live manifest. Another policy allowed a member to add items to an event if the user belonged to the event — also a requirement for the app to be functional. However, the latter policy did not confirm ownership of the item being added. With the policies combined, someone in two events could pull a fellow member's item from one event into the other, exposing its full details to potentially unauthorized users. The fix required an enforcement of item ownership when adding to an event, with tests in place to prevent another leak.

Two people scanning the same item at once. At load-out, several people might scan the same pile. Without care, two simultaneous scans of one item could both read as unreturned, causing the same item to be processed twice. The scan functions now lock the manifest row while deciding; resolving a mystery scan tolerates someone else having resolved it a moment earlier. A test is in place that fires two concurrent resolutions and verifies a result of exactly one manifest row.

Saying too much in an answer. When you scanned an item whose owner wasn't in your event, the first version of process_scan told you the item's name, which is information you weren't otherwise allowed to see. Now it just says the owner isn't part of the event.

A re-load that silently did nothing. Loading an item, unloading it, then loading it again onto the same event left a stale "returned" timestamp behind, so the item still looked unloaded even though it was back on the list. It was a one-line fix in an upsert, thankfully found by a reviewer asking what would occur the second time through.

These discoveries validated how I work with AI tools. Similar to pair programming and the peer review process, utilizing separate Claude Code sessions to analyze each other's work product often uncovers unexpected flaws. Additionally, I do my own review of the spec, implementation plan, and PR as part of a natural QA workflow, which might include reading through a migration's SQL and testing the policies against a real Postgres database.

What I cut

Not every idea survived. The original data model had a "borrowed until" date on each item, so lent-out gear wouldn't raise a false alarm at load-out. In practice, nothing kept that date honest. I removed it and built a real flow instead: if you find someone's stray gear, you scan it and mark "I found this" until it gets back to its owner. Keeping an event's manifest accurate now happens on the load-gear page, so load-out no longer needs a special case for lent gear.

I also accepted a known limitation rather than engineering around it. Supabase's free tier pauses a project after a week of inactivity, and this app is bursty by nature: quiet for weeks, then busy right before a gig. Manually resuming a paused project due to inactivity is an acceptable trade-off for now.

What's next

Gear Tracker is deployed and is actively being tested for real events. Two things from the original spec are still unbuilt:

The biggest takeaway wasn't necessarily about tracking gear. Directing an AI coding agent is similar to leading a team, with time allocated to brainstorming, writing specs, breaking the project down into tasks, leaning on test-driven development, and reviewing everything. The agent wrote most of the code; the process is why I trust it.