An inventory, storage network and operations app for Handlefy
A mobile inventory and storage service where people photographed their belongings, arranged pickup or return, and where the operations team placed each item in a numbered storage unit.
Handlefy was an attempt to give physical possessions the one thing digital files already had: an index. You photographed a sofa, a box of books or a television, named it, put it in a category and could later find it in the app. Then you could ask for it to be collected into storage, or returned. The useful idea went further than a conventional self-storage company. Someone could have objects stored, and a storage owner could make their own space part of the network. The product had to keep those roles and their things legible, even after a van had moved them somewhere the owner could not see.
I worked on the iOS and Android app, and on the browser-based operations CMS with two others. The app was React Native through Expo, backed by Laravel. The CMS was React-admin, pointed at a JSON API. That sounds like a fairly standard split, but it gave us two sharply different interfaces: a calm, phone-sized inventory for a person standing in a room, and a control surface for someone approving pickups, assigning storage units, checking invoices and dealing with the consequences.
A picture was the beginning
The first job had to take seconds. The app could open the camera or photo library, crop an image square and attach it as multipart form data. It also offered the most common object formats on the welcome screen, so a new customer could add several familiar objects without filling in a blank form for each one. A more exact item had a title, count, categories, photo and dimensions. A photo was there to recognise the chair later. The dimensions were there because a storage service cannot price a chair from its name.
There were standard formats with a name, dimensions, icon and monthly price, plus a manual route for anything awkward. The manual form took length, width and height in centimetres. The same app code calculated the custom price from a current formula using a minimum dimension, factor and exponent. It showed the monthly total for the selected items, the minimum one-month term, and then a daily price. The item list was not pretending an estimate was a quote hidden in a later email. It showed the storage cost while you were deciding what to send away.
The app kept the authentication token in device storage and added it to every API request. A 401 cleared those local credentials and returned the person to sign in, instead of leaving an old inventory on screen after their session had ended. Inventory, formats, the pricing formula, orders and invoices were loaded independently, each with its own loading state. That is less glamorous than one enormous “app loaded” switch, but a person could refresh their collection without waiting for an invoice PDF or a list of time slots.
The app was for recording and requesting. The CMS was for deciding when a job could happen and where a completed job physically ended up. The item movement log joined the two.
A physical object has a history
“In storage” could not be a checkbox. It had to be the latest result of a chain of events. The backend treated a deposit as type 0 and a return as type 1. For each item in a completed order it wrote a transaction with the item ID, storage-unit ID, order ID and timestamp. Whether the app showed an item as remote came from the last transaction: the last type 0 meant it was in storage; the last type 1 meant it had come back. The inventory screen therefore grouped local and remote belongings from the actual movements, not from a separate flag somebody could forget to update.
Each transaction also stored a JSON snapshot. It kept the title, photo if present, count, euro currency and rate, then either the standard format and its dimensions or the exact dimensions and pricing formula used for a custom object. That mattered when billing crossed a month boundary or a format price changed. The price visible now did not get to rewrite what an earlier storage movement meant. The billing code can reconstruct a month from transactions and their snapshots, and the invoice itself stores another snapshot.
The constraint was simple and annoyingly physical: one van visit cannot both collect a pile of things from a flat and return a different pile from a warehouse without becoming two jobs. The selection state therefore labelled an item set LOCAL or REMOTE. Local items got the “to storage” action. Remote items got “bring back”. Once a person had selected one kind, the other kind was not selectable for that order. It removes a clever-looking option from the interface and removes an ambiguous job from the driver’s day.
Six steps, then a person says yes
The customer-facing pickup form was six steps. It collected proposed dates and time slots, address, optional service, the selected items and their prices. It also separated the recurring storage total from transport. Transport could include the selected service, a date price and a time-slot price; the backend saved those pieces with the order address. The request did not promise a time it could not keep. It sent the proposed slots to operations, where a real person selected one during approval.
Orders had an intentionally small state machine: processing, approved, complete and rejected. Rejecting an order released its item locks, so belongings did not vanish from the owner’s collection because a slot was unavailable. Completing it required a numbered storage unit. Only then did the backend write the item transactions, clear the temporary order ID from each item and move the order to complete. Returning an object followed the same machinery in the other direction. The app could show the customer the proposed or picked slot, collection or return address, included items, cancellation control while the order was still unconfirmed, and the order history afterwards.
Recurring payments make an ordinary booking flow less ordinary. For a deposit request, the backend checked for a valid Mollie customer mandate. Without one, it created a holding order instead of a regular pickup. The app opened the mandate URL in an in-app browser and asked the API to check again when the person came back. It did not mark a payment instruction valid just because the browser had disappeared. The customer could also read their invoices in the app, opening the PDF returned by the invoice endpoint.
A pickup or return was a request first, an appointment after approval, and a physical move only after the storage unit had been recorded. The customer saw the same distinction in the app.
The screen behind the van
The CMS exposed the entities operations needed rather than a generic database dump: users, items, formats, storage locations, storage units, time slots, orders, invoices and transactions. An order page had buttons to add or remove items, approve, reject, complete and obtain its PDF. Approval opened a dialog with the time slots offered by the customer. Completion opened another dialog that required the storage-unit number. The status changed only if the server accepted the action. The browser UI was not trusted to make an order true by itself.
Formats and pricing rules were editable separately from the app. A rule could have a start and end date and JSON properties, and a price-factor rule could discount the first month without changing later months. When the app loaded an order it received the rules applicable to that order. Operations could issue a payment request, see payment states from Mollie and download the invoice PDF. That was the slightly boring part that turns “we’ll store it” into something that can be reconciled.
The icon that would not behave
The format selector originally used SVG methods for its object icons. They worked badly across the mobile setup, so I replaced them with PNGs and added a converter. It is a very small failure, but it mattered because this was a first-use screen: a sofa, box or bike icon needed to appear before anyone could use it as a shortcut. The later iPad correction was in the same area. Standard-format icons were a convenience, not a reason to make the manual dimensions route unusable.
The app was published through Expo for iOS and Android. Its last archived version identifies itself as 1.8.5 with Android version code 22. The product and its public site are offline now, so the images here come from the supplied 2018 article image and the supplied product film. They show test furniture and app screens, not a customer’s inventory.