<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: gatekeeperzero</title>
    <description>The latest articles on DEV Community by gatekeeperzero (@gatekeeperzero).</description>
    <link>https://dev.to/gatekeeperzero</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4207272%2F3cd6e88d-67f9-4702-bec7-aea7516550be.jpg</url>
      <title>DEV Community: gatekeeperzero</title>
      <link>https://dev.to/gatekeeperzero</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gatekeeperzero"/>
    <language>en</language>
    <item>
      <title>Building an offline-first guard app for Indian housing society gates</title>
      <dc:creator>gatekeeperzero</dc:creator>
      <pubDate>Sun, 25 Oct 2026 19:57:59 +0000</pubDate>
      <link>https://dev.to/gatekeeperzero/building-an-offline-first-guard-app-for-indian-housing-society-gates-c0g</link>
      <guid>https://dev.to/gatekeeperzero/building-an-offline-first-guard-app-for-indian-housing-society-gates-c0g</guid>
      <description>&lt;p&gt;A security guard at a housing society gate in India deals with a visitor every few minutes: guests, delivery riders, cabs, house help. The gate is often at the edge of the plot, under a concrete canopy, on a low-cost Android phone with patchy mobile data. If the app stops logging when the network drops, the guard goes back to the paper register, and the society loses its record of who came in.&lt;/p&gt;

&lt;p&gt;So when we built the guard app for &lt;a href="https://gatekeeperzero.com/visitor-management-system-for-society" rel="noopener noreferrer"&gt;GateKeeperZero&lt;/a&gt;, "keeps working offline" was a hard requirement, not a feature. Here is how it works.&lt;/p&gt;

&lt;h3&gt;
  
  
  The rule: write locally first, sync later
&lt;/h3&gt;

&lt;p&gt;Every gate action (a visitor check-in, a delivery, a house-help entry) goes through one function: &lt;em&gt;submit directly when online, queue when offline or when the request fails for a temporary reason&lt;/em&gt;. The guard sees the same "done" either way. The difference is only where the record waits.&lt;/p&gt;

&lt;p&gt;The queue is a small Zustand store, persisted to MMKV so it survives the app being killed. Connectivity comes from &lt;code&gt;@react-native-community/netinfo&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;QueuedItem&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;            &lt;span class="c1"&gt;// e.g. 'visitor-checkin', 'delivery-checkout'&lt;/span&gt;
  &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;
  &lt;span class="nx"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="c1"&gt;// generated once, when the guard taps the button&lt;/span&gt;
  &lt;span class="nx"&gt;queuedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
  &lt;span class="nx"&gt;retryCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
  &lt;span class="nx"&gt;lastAttempt&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Idempotency keys stop double entries
&lt;/h3&gt;

&lt;p&gt;The hardest bug in offline sync is not losing data. It is &lt;em&gt;duplicating&lt;/em&gt; it. A request can reach the server, get saved, and the response can still be lost on the way back. The app thinks it failed and retries.&lt;/p&gt;

&lt;p&gt;Each queued item therefore carries an idempotency key created at the moment the guard acted, not at send time. The server keeps each key for 24 hours (the longest an item is allowed to wait offline) and answers a repeat with the original result instead of creating a second entry; the batch response lists the items it saw as duplicates. Retrying becomes safe, so the client can retry freely.&lt;/p&gt;

&lt;h3&gt;
  
  
  Not every failure deserves a retry
&lt;/h3&gt;

&lt;p&gt;Retries are capped (five attempts), and failures are split in two:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Temporary:&lt;/strong&gt; no network, timeouts, server errors. The item stays in the queue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permanent:&lt;/strong&gt; the server says the request itself is wrong, or the app version is too old for the API. Retrying won't help, so the item moves to a &lt;strong&gt;dead-letter queue&lt;/strong&gt; and the guard is told it needs attention.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Draining the queue in the background
&lt;/h3&gt;

&lt;p&gt;Guards don't open the app just to sync. A background task (&lt;code&gt;expo-task-manager&lt;/code&gt; + &lt;code&gt;expo-background-fetch&lt;/code&gt;) drains the queue when the OS wakes the app, and the queue also drains when NetInfo reports the connection is back. Items that map to the same API call go to a batch sync endpoint. A few flows that need two steps (verify a code, then check in) are handled one by one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Personal data on a shared phone
&lt;/h3&gt;

&lt;p&gt;A gate phone is shared between shifts, so the queue holds visitor names and phone numbers for as short a time as possible:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dead-letter items auto-expire, and after the first day their payload is stripped down to the type and the failure reason.&lt;/li&gt;
&lt;li&gt;Logging out clears the queue. A cancel token stops a drain that is already running from writing the queue back to disk after the clear, which was a real race we had to close.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What we would tell anyone building the same thing
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Decide the idempotency strategy before you write the first queue item.&lt;/li&gt;
&lt;li&gt;Classify failures early; "retry everything" fills phones with requests that will never succeed.&lt;/li&gt;
&lt;li&gt;Treat the on-device queue as personal data, with an expiry and a clear-on-logout path.&lt;/li&gt;
&lt;li&gt;Test with the network off at the moment the response is coming back, not just before the request.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you work on software for Indian housing societies, or just like offline-first problems, we'd like to hear how you handle sync conflicts. More on what the guard app does at the gate: &lt;a href="https://gatekeeperzero.com" rel="noopener noreferrer"&gt;gatekeeperzero.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>offline</category>
      <category>mobile</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
