I released offline support for my business app. Users can keep working without a signal, and when they come back online the app syncs with the server automatically.
Honestly, this was a feature I had known I needed but had given up on until now. The implementation load was just too heavy.
After shipping it, a strange feeling stayed with me. Usually I have a sense of "this is code I understand down to here," but this time there was clearly more on the product than my head could follow. It still works properly. A little scary, a little exciting.
Stories about generative AI speeding up development are common now. But this time it was not just faster—I shipped a feature that had been out of reach and made the product noticeably better.
This is not a story that "with AI, you can ship hard features without knowledge." What felt most interesting was that developer knowledge and experience were boosted by AI.
Offline is normal at job sites
Business apps used in warehouses, kitchens, delivery stops, and event venues often have unstable connectivity. But work on site does not stop because the signal is bad.
Until now, in poor signal areas I asked people to wait, or temporarily saved data in simple local storage and reconciled manually later. But if the app closes mid-operation, or another device touches the same data, consistency breaks.
For example: "inventory went down but no history was recorded," or "the connection dropped so I resent and processed the same item twice." In business apps, that is genuinely bad.
So I needed real offline support: keep operating without a network and sync automatically when connectivity returns. Reading the requirements alone, it sounds ordinary. Implementing it is anything but.
Service Worker did not fit my requirements
When people hear offline for web apps, Service Worker often comes to mind first. It is good at caching HTML, JavaScript, and similar assets so the app can open without a network.
There is also the Background Sync API to resume communication when back online. However, it is not supported in Safari—not Service Worker itself, but SyncManager, which runs sync after coming online, is unavailable.
But a bigger problem than Safari was what Background Sync actually provides: a trigger to run processing in a Service Worker after going online. You still have to implement sending requests and reflecting results locally yourself.
What I needed was not just sending operations to the server. I had to reflect processing results into SQLite on the device and keep data used for display and printing up to date. When updates from another device conflicted, I had to decide what to keep. I needed application-level sync that composes business data, applies it, and resolves conflicts—not just retrying communication.
MySQL and SQLite could not be copied as-is
The server uses MySQL with normalized tables. On the device I use SQLite. At first I thought I could copy MySQL tables straight into SQLite—but it is not that simple.
On the server, product master, label templates, inventory history, printing settings, and so on live in separate tables. On the device, I wanted to avoid JOINing repeatedly every time the app renders a screen or prints. Offline, it is easier to predict behavior if needed information is consolidated.
So I built a conversion layer that composes multiple server tables when sending to the device, and decomposes updates when sending back from the device.
Multiple MySQL tables
↓ Compose
SQLite print data
↓ Operate offline
On-device operation queue
↓ Decompose after reconnecting
Apply changes to MySQL
Locally you read and write one simple record; on the server I keep the existing table structure. Looked at in isolation, it is a clean design.
The problem is that compose/decompose mapping becomes enormous.
From dozens of MySQL tables to SQLite composite records and back again—you have to align column meaning, defaults, foreign keys, triggers, history handling, everything. Hand-writing conversion code guarantees mistakes somewhere.
Connections drop at the worst moments
Operations during offline naturally do not reach the server immediately. So I queue operations on the device in small units and process them in order when online again.
What I cared about here was not leaving a "half succeeded" state.
Processing that should complete as one business action either succeeds together or fails together. When syncing to the server I wrap operations in transactions; if communication drops mid-way, the local queue remains. On the next recovery, processing resumes from there.
Idempotency matters too. Resending because the connection dropped must not reduce inventory twice.
Also, while a device is offline, another device may update the same data. Patterns include: app killed just before sync, server finished but communication dropped before local reflection, local updated but server failed, and more.
In words it is a few lines. Once you start combining "when did it drop," "how far did it succeed," and "what did another device do in between," there is no end.
AI turned developer knowledge into implementation and verification volume
Implementing offline sync at this scale without AI would take me months. Investigation when bugs appear is especially heavy.
So I delegated work like this to the LLM:
- Generate mapping code from schema diffs
- Enumerate state combinations and generate test cases
- Trace operation logs and infer bug causes
- Detect code that might succeed only partway
Bug investigation was where the LLM was strongest.
When I said "after this operation, this value here is wrong," it cross-checked the operation queue and server transaction logs and narrowed down where consistency broke in minutes. Investigations that used to take hours now got an initial suspect quickly. That helped enormously.
However, I did not treat LLM answers as automatically correct.
Why data shape differs between MySQL and SQLite, how much belongs in one transaction, which value to keep on conflict—AI can produce lots of code and tests, but it does not decide on its own which state is correct for the business.
I shifted time from implementation and investigation to design and judging outcomes. A closer description is that developer knowledge and experience were converted by AI into implementation and verification volume.
Roughly by numbers: it is not that someone with zero knowledge suddenly builds 100 products with AI, but that a developer with strength 100 can now reach 300 in implementation and verification that used to be out of hand. That might be what generative AI boost means.
Why I could release without tracing every line of code
This was the most interesting part for me personally.
In development until now, one basis for quality was "the implementer understands the code." I still understand the design this time, and I review important code. But including LLM-generated conversion logic, tests covering state combinations, and failure analysis, I cannot hold everything in one head.
I still released it.
Because even without tracing every line, I could define boundaries AI must not cross:
- I define the boundaries to protect
- Turn those boundaries into my ESLint rules and tests
- Verify outcomes from actual behavior and logs
- Stop in CI when AI output crosses a boundary
Not just asking in a prompt "write it like this," but making wrong output stop mechanically. I wrote about this before in Custom ESLint Rules for AI Coding: Generic Rules Cannot Control AI Coding—the prompt is the manual; ESLint is the guardrail.
You do not have to memorize every turn if you can mechanically confirm you have not crossed the guardrails you set. Of course that alone does not guarantee quality; tests, review, and real-device verification are still required separately. I could release because I could judge using these together.
So this is not "you do not need to read AI-written code." Without guardrails and verification, "beyond my understanding" is not an outcome—it is just danger.
Summary
I released offline support for a business app I had shelved because the implementation load was too heavy. I connect MySQL and SQLite through compose and decompose, and avoid leaving half-finished states no matter where communication drops.
What I learned: AI does not make developer knowledge unnecessary—it widens how far that knowledge reaches. And even without tracing every line of code, you can keep a product under control by combining design, guardrails, tests, review, and real-device checks.
Precisely because AI lets me build what my head alone cannot follow, mechanisms to keep that manageable will matter more from here on.
See you next time.