Apps & platform
The same app on six platforms, and it keeps working when the network does not
A business app is judged on its worst moment, not its best one. The interesting engineering is not the screen that loads in two hundred milliseconds — it is the shift that carries on when the connection drops, the ticket that still reaches the right printer, and the notification that arrives in the language the person reading it actually speaks.
Akkad's admin app is one codebase compiled to six targets. Not a mobile app with a cut-down web viewer bolted on, and not a desktop build that is really a wrapped website — the same screens, the same logic and the same offline behaviour everywhere, which is why a feature does not arrive on one platform three months before another.
The practical consequence is that a business does not have to choose. The owner works on a laptop, the counter runs on a desktop machine with a scanner plugged into it, the delivery coordinator uses a phone, and someone checking a figure on a borrowed computer opens a browser. All of them are looking at the same application.
01
Where the app runs
| Target | Notes |
|---|---|
| iOS | Native build, with an in-app store version check and a per-version dismiss so an update prompt cannot become a permanent nag |
| Android | Native build, same version check, and verified deep links so a shared install link opens the app rather than a browser page |
| Web | Full application in the browser, installable as a progressive web app with a standalone display mode and maskable icons |
| macOS | Native build with automatic updates delivered through a signed update feed |
| Windows | Native build with the same automatic update mechanism, on its own per-app feed |
| Linux | Native build from the same codebase |
Desktop updates are signed. The macOS build carries a public key and verifies the signature on every update it downloads, so an update feed cannot be substituted for a hostile one. Each vertical points at its own feed, set at runtime, so the pharmacy app can never be handed the restaurant app's release.
02
The showpiece
Working with no connection
Offline mode is not a cached read-only view. The device holds a real local database — the catalogue with its variants, images and pack sizes, your customers, today's orders, pinned quick-sale items and, for restaurants, the tables — and the app keeps taking orders against it. Everything that happens while disconnected joins a queue, and the queue is designed around one assumption: the network will come back, and until it does nothing may be lost.
The catalogue is searchable, not just listed
The local database is indexed on name, barcode and SKU, so scanning and searching behave the same offline as on. A cashier does not discover the limits of offline mode mid-transaction.
Some things are blocked, deliberately
Creating an item offline, or editing an order that already exists on the server, would produce conflicts that can only be resolved by guessing. Those actions are refused with a clear message rather than queued and reconciled badly later.
Retries never give up on a transient failure
There is no attempt counter that quietly discards work. Genuine rejections park in a distinct state; anything that merely failed to reach the server is retried on every subsequent flush, indefinitely. The flush cadence is the backoff.
Nothing is ever deleted
After around ten rejections across roughly two and a half hours of working connectivity, an order moves to a visible needs-attention state with a one-tap manual retry. It is surfaced to a human, not thrown away. That is the whole policy, and it does not have exceptions.
03
What works offline, and what does not
The line is drawn at conflict. If an action can be replayed later without ambiguity, it works offline. If replaying it would require guessing what happened in between, it is blocked.
| Works offline | Blocked offline |
|---|---|
| Browse and search the full cached catalogue by name, barcode or SKU | Creating, updating or deleting items |
| Browse cached customers | Creating, updating or deleting customers |
| Create orders — queued, and visible in the orders list straight away | Updating or deleting an order that already exists on the server |
| Open a cashier session, including when the shift starts offline | Bulk import |
| Close a cashier session, including one that was opened on the server | Bulk status updates |
| Add dine-in rounds to an order the server already knows about | Exporting items or orders |
| Delete an order that is still only queued locally |
A blocked action explains itself in the moment rather than failing silently, so nobody spends a minute wondering whether the tap registered.
04
The flush, in four phases
When the connection returns, the queue is not sent all at once. It is sent in a strict order, because the pieces depend on each other. The flush runs on app start, on connectivity recovery, and on a fifteen-minute background timer, and it is guarded against running twice at the same time.
- 1
Session opens
Any shift that was opened while offline is created on the server first, so it has a real server identity before anything is attached to it.
- 2
Orders
Queued orders are submitted next, and they attach to the session identity that phase one just established. If orders went first they would either attach to a local identity that never becomes real, or attach to nothing at all — and a drawer that cannot account for its own sales is worse than no drawer report.
- 3
Dine-in rounds
Rounds added to existing orders are appended after the orders exist. Each carries its own idempotency key, so a round that was sent and whose response was lost is not appended twice.
- 4
Session closes
Only once every order belonging to a shift has landed is the shift closed. The close computes the drawer from orders that are all actually present, so the expected cash figure is right the first time.
05
Why the queue never loses anything
Four decisions, each of which exists because the obvious alternative destroys data in a way nobody notices until much later.
Lost responses adopt the existing session
If a session open is rejected because the server already created it — a request that arrived, succeeded, and whose response never made it back — the app looks up the user's current open session and adopts it rather than treating the whole thing as failed. Without this, one lost response would strand every queued order behind a session that could never be created.
Deletion propagation has a fail-safe
Every sync returns the complete set of live item and customer identifiers, and the device prunes anything cached that is no longer in that set, in batches. If that list arrives missing, malformed or empty, pruning is skipped entirely. An empty list is far more likely to be a partial response than a business that deleted its entire catalogue, and acting on it would leave a cashier with nothing to sell.
Going offline is debounced, coming back is not
A three-second debounce on going offline stops a moment of poor signal from flipping the whole app into offline mode and back. Recovery is immediate, because there is no reason to wait once the connection is real.
There is a recovery probe for lying networks
When a low-level network error forces a hard offline flip, a probe loop runs every ten seconds until it gets a real answer. Operating systems sometimes report a healthy connection through a captive portal or a dead uplink; the app checks for itself rather than believing them.
06
Staying current
Signed desktop updates
macOS and Windows update themselves from a per-app feed set at runtime. The macOS build verifies an update's signature against a bundled public key before installing it.
A store prompt that respects you
Mobile builds check the store for a newer version and offer it, with a dismissal remembered per version — so declining once does not mean being asked again the same afternoon.
Deep links that open the app
Verified links on Android and associated domains on iOS mean a shared download link resolves into the app when it is installed, instead of bouncing through a browser page first.
A What's New sheet
Shown once per version after an update, localised, and seeded quietly on a fresh install so a new user is not greeted by release notes for software they have never used.
One full sync after every update
An app that has just changed shape re-fetches everything once rather than trusting a cache written by the previous build.
Installable in the browser
The web build ships a manifest with a standalone display mode, theme colour, and both standard and maskable icons — so a shop machine can run it as an application rather than a tab.
07
Three print layouts
Each layout has its own paper sizes, and each is configured separately — an invoice and a thermal receipt are different documents with different jobs.
| Layout | Paper | Typical use |
|---|---|---|
| Invoice | A4 or US Letter | The document that goes to a business customer or into the file |
| Shipping label | 100 × 150 mm or 80 × 120 mm | What the courier scans and reads at the door |
| Receipt | 80 mm or 58 mm thermal roll | The slip handed over at the counter |
Thermal output is sized to the print head rather than the paper, which is the single most common source of receipts with a cut-off right edge. An 80 mm roll gets 576 dots across 72 mm at 203 DPI; a 58 mm roll gets 384 dots across 48 mm. Printers connect over the network using ESC/POS on the standard port, or over USB.
08
Print controls
Around forty-five individual toggles across the three layouts, because what belongs on a printed document is a business decision and not a template author's.
Every field is optional
Organisation logo and shipping-agent logo, name, city, address, postal code, phone and email; the same set for the customer; totals, order number, tracking number, who created it, notes, package weight and dimensions, tax, discount, custom amounts and custom fields. QR code or barcode, keyed to the order number or the tracking number — and mutually exclusive on the invoice, because both is neither.
The items table is a set of columns
Item name, image, SKU, quantity, unit price and total price are each their own toggle. Turning all of them off hides the table entirely, which is a legitimate document — a delivery note that deliberately does not list prices.
Kitchen station printers
For restaurants: named network printers, each mapped to a set of categories, so the grill ticket goes to the grill. A fallback station catches any category that is not mapped anywhere, because a dish that silently never prints is the worst possible failure in a kitchen. Each station can optionally list the rest of the round as context. Network-only by design, since stations sit nowhere near the counter.
Bulk label printing
Pick items, set a print quantity per item, and get one page per label sized exactly to the label. Code type is QR, barcode or none; width and height are set in millimetres, defaulting to 38 × 25 and clamped between 10 and 300. Name, SKU and price are individually shown or hidden, and the settings persist on the device.
Right-to-left detected from the content
Print output has its own right-to-left layer, and label printing detects direction from the text itself as well as from the app language. An English interface printing Arabic item names still renders them with the correct font and direction — which is the normal case, not an edge one.
The logo is treated like an icon
Uploaded logos are trimmed, given a rounded mask, capped on the long side and cached once per session rather than re-decoded for every job. A logo that was fine on screen does not arrive on paper as a stretched rectangle in a white box.
09
What triggers a notification
Every notification is localised to the recipient's own language and mirrored into a persistent in-app inbox, so nothing depends on someone having seen a banner at the right moment.
| Trigger | Who receives it | When |
|---|---|---|
| New order | Team members allowed to see that order's creator — including a pseudo-creator covering public storefront and landing-page orders | On creation. A member restricted from seeing an order is not sent a push about it |
| Low stock and out of stock | Organisation owners | On the stock change, one alert per item |
| Batch expiry digest | Owners plus anyone holding the expiry report permission | Daily at 06:00 UTC, roughly 09:00 in Iraq. Two tiers: batches hitting the seven-day mark, and batches expiring today |
| Onboarding nudges | Trial users | Daily at 16:00 UTC, early evening in Iraq — one nudge, chosen by priority |
| Subscription notices | Owners | Daily at 08:30 UTC |
The expiry digest needs no deduplication table. Because it matches on the exact day in the organisation's local time, and because it runs once a day, the daily run is the deduplication — a batch can only hit its seven-day mark on one day. Removing the need for state is usually better than managing the state well.
10
Twelve nudges, in priority order
A new business signing up has a specific sequence of things to do, and the order matters — there is no point suggesting shipping rules to someone who has not added an item. So the nudge system holds twelve messages in strict priority order and sends the highest-priority one that still applies: add items, create orders, set up categories, try bulk import, add a team member, try scanning, look at the analytics, customise the business details, check the invoice layout, set up the website, configure shipping rules, set tax.
Which one applies is decided by looking at the account itself — how many items, orders and categories exist, how large the team is, whether a logo has been uploaded, whether tax has been configured, and how long ago each of those was created. Each nudge carries a destination, so tapping it lands on the screen where the thing is actually done rather than on a home page with instructions.
11
Thirteen scheduled jobs
The work that happens whether or not anyone has the app open. Each job is wrapped so that a crash is recorded as a queryable failure event rather than disappearing into a log nobody reads.
| Job | Cadence |
|---|---|
| Onboarding nudges | Daily at 16:00 UTC |
| Batch expiry digest | Daily at 06:00 UTC |
| Subscription notices | Daily at 08:30 UTC |
| Expired-data cleanup | Every 2 hours |
| Notification cleanup | Daily at 02:30 |
| Inactivity cleanup — warn, then delete organisations inactive 12 months | Daily at 03:00 |
| Cashbox lifecycle | Daily at 01:00 |
| Monitoring retention — events over 30 days, request logs over 14 | Daily at 02:45 |
| Subscription trial sync | Every 6 hours |
| Custom-domain certificate checker | Every 5 minutes |
| Product catalogue sync | Every 5 minutes |
| Deferred purchase events | Every minute |
| Location cache refresh | Monthly, on the 1st at 02:00 |
12
Thirteen languages, fourteen on storefronts
The app ships in English, Arabic, German, Spanish, Persian, French, Hindi, Indonesian, Portuguese, Russian, Turkish, Urdu and Chinese. Storefronts add a fourteenth, Kurdish Sorani, because the people buying from an Iraqi shop are not always reading the same language as the people running it.
Four of those are right-to-left: Arabic, Persian, Urdu, and Kurdish Sorani on storefronts. Direction drives layout throughout rather than being applied as a stylesheet afterwards — the navigation curve, the chevrons and the list separators all mirror. Two Arabic typeface families are bundled at four weights each, so Arabic text is set properly rather than falling back to whatever the device happens to have.
Localisation reaches past the interface. Every API error carries a message resolved into the calling user's own language, every push notification is localised per recipient rather than per organisation, and stock history is stored as structured keys and translated at the moment of reading — so the same log entry renders in Arabic for one colleague and English for another. Printing carries its own right-to-left layer, with fonts preloaded for every supported print language.
13
Questions
If the internet drops mid-shift, what actually happens?
The app switches to the local database after a three-second debounce and keeps taking orders. Everything created while offline queues locally and appears in the orders list immediately, so the shift looks normal. When the connection returns, the queue flushes in four phases — sessions, orders, rounds, then session closes — and nothing is deleted at any point.
Can two people take orders offline on different devices?
Yes. Each device holds its own queue and its own cashier session, and each flushes independently when it reconnects. Order creation carries an idempotency key, so a submission whose response was lost returns the original order rather than creating a second one.
Why can't I create an item while offline?
Because uniqueness on SKUs and barcodes is enforced across the whole organisation, and two disconnected devices cannot both guarantee it. Rather than accept the item and reject it hours later during a flush — after it has already been sold — the action is blocked with a message at the moment you try.
Does my thermal receipt need configuring for paper width?
You choose 80 mm or 58 mm and the printable area is derived from the print head, not the paper. That is why receipts do not come out with the right column clipped, which is the usual symptom of a layout sized to paper width instead.
How do kitchen tickets know which printer to use?
Each station printer is mapped to a set of categories, and a round's lines are routed accordingly. A category mapped to nothing goes to the fallback station rather than being dropped — in a kitchen, a ticket that silently never printed is the failure that costs a table.
Will my team get notifications in their own languages?
Yes. Notifications are localised per recipient rather than per business, so a mixed team each reads the same alert in their own language, and the same is true of API error messages and of stock history.
Related
Counter & cashbox
Drawer sessions, scanning and the shift close that the offline flush is ordered around.
Orders
Idempotent creation, editing with a change log, and the statuses the queue replays into.
Team & permissions
The grants that gate printing and reports, and the visibility rules notifications honour.
See it against your own catalogue
Start on the plan that fits, import your products from a spreadsheet, and have a working store, counter and order book the same day.
Free tier available. No card required to try. Import brings your existing catalogue with you.