<?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>Forem: Seleziomar Júnior</title>
    <description>The latest articles on Forem by Seleziomar Júnior (@seleziomar).</description>
    <link>https://forem.com/seleziomar</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3820841%2F772fc3a8-6460-4f34-b5b6-0768c5e9651c.png</url>
      <title>Forem: Seleziomar Júnior</title>
      <link>https://forem.com/seleziomar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/seleziomar"/>
    <language>en</language>
    <item>
      <title>App React Native offline-first</title>
      <dc:creator>Seleziomar Júnior</dc:creator>
      <pubDate>Thu, 12 Mar 2026 22:33:01 +0000</pubDate>
      <link>https://forem.com/seleziomar/app-react-native-offline-first-4ii6</link>
      <guid>https://forem.com/seleziomar/app-react-native-offline-first-4ii6</guid>
      <description>&lt;p&gt;&lt;strong&gt;Como construí um app mobile offline-first para gerenciar check-ins de milhares de pessoas em eventos — e por que cada decisão de arquitetura nasceu de um problema real&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Imagina o cenário: um evento corporativo com 2.000 participantes. Staff espalhado por checkpoints de atividades, transporte, palestras, brindes, bagagem. Cada pessoa precisa ter seu check-in registrado em tempo real. O problema? O Wi-Fi do centro de convenções é instável, a rede 4G oscila, e se o app trava, o evento não espera.&lt;/p&gt;

&lt;p&gt;Foi esse o desafio que me levou a construir um app React Native com Expo que precisou ser pensado do zero para funcionar mesmo quando a internet decide não colaborar.&lt;/p&gt;

&lt;p&gt;Vou compartilhar as decisões técnicas e os problemas reais que cada uma resolve.&lt;/p&gt;




&lt;h3&gt;
  
  
  A Arquitetura
&lt;/h3&gt;

&lt;p&gt;A estrutura do projeto foi organizada para separar responsabilidades de forma clara. Cada camada tem um papel bem definido:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/majoo-staff
├── /app                              # Rotas (Expo Router - file-based)
│   ├── _layout.tsx                   # Layout raiz: tema, auth, providers
│   ├── (unauthenticated)/            # Login, recuperação de senha
│   └── (authenticated)/              # Telas protegidas
│       ├── Home/                     # Dashboard com categorias
│       ├── checkpoints/
│       │   ├── [type]/index.tsx      # Lista por categoria
│       │   └── [type]/[id]/          # Detalhe + lista de usuários
│       │       └── qrcode/           # Scanner QR
│       ├── search/                   # Busca global
│       ├── checkins/                 # Histórico
│       └── reports/                  # Relatórios com export PDF
│
├── /components                       # UI reutilizável
│   ├── /Button                       # Variantes (primary, float, checkin)
│   ├── /Input                        # Text, search, underline
│   ├── /Layout                       # Headers, Footers, Profile
│   ├── /Modal, /Loader, /Hr
│   ├── ThemedText.tsx
│   └── ThemedView.tsx
│
├── /hooks                            # Lógica de negócio encapsulada
│   ├── useApi.ts                     # Wrapper HTTP com Bearer token
│   ├── useApplications.ts           # Gestão de eventos + importação
│   ├── useCheckpoints.ts            # Check-ins + sincronização
│   └── useReports.tsx               # Geração de PDF
│
├── /contexts
│   └── AuthContext.tsx               # Estado global: user, app, auth
│
├── /models                           # Camada de dados (SQLite)
│   ├── useCheckin.ts                # CRUD de check-ins locais
│   └── useUsers.ts                  # Queries com JOINs relacionais
│
├── /utils
│   ├── db.js                        # Schema SQLite + índices
│   ├── types.ts                     # Tipos TypeScript
│   ├── helpers.ts                   # Criptografia, storage, formatação
│   └── validations.ts              # Validações de entrada
│
└── /constants
    ├── CheckpointTypes.tsx          # Categorias com ícones
    └── Colors.ts                    # Tema claro/escuro
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A decisão de usar &lt;strong&gt;Expo Router com route groups&lt;/strong&gt; (&lt;code&gt;(authenticated)&lt;/code&gt; e &lt;code&gt;(unauthenticated)&lt;/code&gt;) resolve um problema clássico: o guard de autenticação vive no layout, não espalhado por cada tela. Se o usuário não está logado, o redirect acontece em um único ponto.&lt;/p&gt;

&lt;p&gt;Os &lt;strong&gt;hooks customizados&lt;/strong&gt; (&lt;code&gt;useCheckpoints&lt;/code&gt;, &lt;code&gt;useApplications&lt;/code&gt;, &lt;code&gt;useReports&lt;/code&gt;) funcionam como a camada de serviço do app. Eles encapsulam API, banco local e lógica de negócio — os componentes de tela ficam limpos, só consomem dados e renderizam.&lt;/p&gt;

&lt;p&gt;Os &lt;strong&gt;models&lt;/strong&gt; (&lt;code&gt;useCheckin&lt;/code&gt;, &lt;code&gt;useUsers&lt;/code&gt;) isolam o acesso ao SQLite. Nenhum componente faz query direta no banco. Isso cria uma camada de abstração que permite trocar a estratégia de persistência sem reescrever telas.&lt;/p&gt;




&lt;h3&gt;
  
  
  Offline-First: onde o app realmente se diferencia
&lt;/h3&gt;

&lt;p&gt;Como requisito, o app deveria funcionar independente do status da conexão. É muito comum que em eventos e locais mais remotos não tenha uma conexão estável, então era muito necessário que o staff conseguisse ter acesso a lista em fazer checkin mesmo que não houvesse uma conexão.&lt;/p&gt;

&lt;p&gt;A solução foi inverter a lógica: &lt;strong&gt;o banco local é a fonte de verdade, a API é apenas o mecanismo de sincronização&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Importação de dados com paginação e transações atômicas&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Quando o staff se conecta a um evento, o app importa toda a base de usuários da API para o SQLite local. A importação é paginada e recursiva, processando cada página dentro de uma transação atômica:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;saveUsers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

  &lt;span class="c1"&gt;// Busca uma página de usuários da API&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/users?page=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Transação atômica: ou salva tudo da página, ou não salva nada&lt;/span&gt;
  &lt;span class="nx"&gt;database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withTransactionSync&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

      &lt;span class="c1"&gt;// Verifica se o usuário já existe no banco local&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;exist&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="c1"&gt;// ... consulta por ID&lt;/span&gt;

      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;exist&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Usuário novo: insere com todo o histórico de check-ins da API&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Usuário já existe: faz merge dos check-ins novos&lt;/span&gt;
        &lt;span class="c1"&gt;// sem sobrescrever os que foram feitos offline&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Se ainda tem páginas, chama recursivamente até importar tudo&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;last_page&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;saveUsers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O merge inteligente garante que &lt;strong&gt;check-ins feitos offline nunca são sobrescritos&lt;/strong&gt; quando a base é atualizada do servidor. Se o staff fez 50 check-ins sem internet e depois importa a base atualizada, nenhum desses 50 registros se perde.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Check-in local imediato — zero dependência de rede&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Quando o staff registra um check-in (manual ou via QR Code criptografado com AES), o dado vai direto pro SQLite — sem esperar resposta da API:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;checkin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;checkpoint_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

  &lt;span class="c1"&gt;// Verifica se já fez check-in nesse checkpoint (evita duplicidade)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;exist&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="c1"&gt;// ... consulta no banco local&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;exist&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;checked&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Salva direto no SQLite com synced = 0 (pendente de envio)&lt;/span&gt;
  &lt;span class="c1"&gt;// Nenhuma chamada de rede acontece aqui — feedback instantâneo&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;saved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="c1"&gt;// ... insert no banco local&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;saved&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Tenta sincronizar em background — não bloqueia o operador&lt;/span&gt;
    &lt;span class="nf"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O feedback pro operador é instantâneo. O check-in aparece na lista na hora. A rede não é bloqueante. Se não tem internet, o dado fica na fila local com &lt;code&gt;synced = 0&lt;/code&gt; e será enviado quando a conexão voltar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Sincronização inteligente com drain recursivo&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;O &lt;code&gt;sync()&lt;/code&gt; é o coração do sistema offline:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sync&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

  &lt;span class="c1"&gt;// Só tenta sincronizar se tiver internet&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;network&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Network&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getNetworkStateAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;network&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isConnected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Busca check-ins pendentes em lotes de 20&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;checkins&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="c1"&gt;// ... SELECT WHERE synced = 0 LIMIT 20&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="c1"&gt;// ... COUNT total de pendentes&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;checkins&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Envia o lote inteiro pro servidor&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="c1"&gt;// ... POST /checkins&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="c1"&gt;// Caso a requisição seja efetuada com sucesso &lt;/span&gt;
    &lt;span class="c1"&gt;// recebemos um feedback do servidor informando &lt;/span&gt;
    &lt;span class="c1"&gt;// quais os checkins foram sincronizados. &lt;/span&gt;
    &lt;span class="c1"&gt;// Caso algum em específico falhe, &lt;/span&gt;
    &lt;span class="c1"&gt;// ele não volta na lista e continua na fila &lt;/span&gt;
    &lt;span class="c1"&gt;// para ser sincronizado no futuro.&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="c1"&gt;// Faz update do checkin para sync = 1&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Se ainda tem mais pendentes, agenda outra rodada em 500ms&lt;/span&gt;
    &lt;span class="c1"&gt;// Esvazia a fila progressivamente sem travar a UI&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O ponto-chave: o sync roda em lotes de 20 com intervalo de 500ms entre eles. Isso garante que mesmo com centenas de check-ins acumulados offline, a fila é esvaziada progressivamente sem travar a UI do operador.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Modelo de dados relacional otimizado&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;O banco local usa duas tabelas com índices estratégicos para as queries mais frequentes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;document&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;application_id&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;-- A qual evento pertence&lt;/span&gt;
    &lt;span class="n"&gt;checkin&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;                            &lt;span class="c1"&gt;-- Histórico de check-ins (JSON da API)&lt;/span&gt;
    &lt;span class="c1"&gt;-- ...&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;checkins&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="n"&gt;AUTOINCREMENT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                       &lt;span class="c1"&gt;-- Quem fez check-in&lt;/span&gt;
    &lt;span class="n"&gt;checkpoint_id&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                 &lt;span class="c1"&gt;-- Em qual checkpoint&lt;/span&gt;
    &lt;span class="n"&gt;synced&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;              &lt;span class="c1"&gt;-- 0 = pendente | 1 = enviado ao servidor&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'now'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;-- ...&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Índices para acelerar as buscas mais comuns&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;application_id_idx&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;application_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;user_id_idx&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;checkins&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;checkpoint_id_idx&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;checkins&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;checkpoint_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;synced_idx&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;checkins&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;synced&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A flag &lt;code&gt;synced&lt;/code&gt; é o coração do mecanismo offline: &lt;code&gt;0&lt;/code&gt; significa "registrado localmente, aguardando envio", &lt;code&gt;1&lt;/code&gt; significa "confirmado pelo servidor". Buscar "todos os usuários pendentes no checkpoint X" é um simples &lt;code&gt;LEFT JOIN&lt;/code&gt;. SQL puro, performático, confiável.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Cache com fallback automático em todas as camadas&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;O padrão se repete em toda a camada de dados:&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="kd"&gt;const&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

  &lt;span class="c1"&gt;// Tenta buscar da API&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="c1"&gt;// ... GET /checkpoints&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// API respondeu: salva no cache local e retorna dados frescos&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// API falhou (sem internet, timeout): busca do cache local&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// O app nunca retorna vazio — sempre tem um fallback&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Isso vale para listagem de eventos, checkpoints, categorias, identificações. O app nunca mostra tela vazia por falta de rede — sempre tem um fallback local.&lt;/p&gt;




&lt;p&gt;Conclusão&lt;/p&gt;

&lt;p&gt;Construir um app offline-first não é apenas adicionar cache — é mudar a arquitetura. O banco local passa a ser a fonte de verdade, enquanto a API vira apenas um mecanismo de sincronização.&lt;/p&gt;

&lt;p&gt;Se você está construindo apps que precisam funcionar em cenários de conectividade instável — eventos, campo, indústria — invista tempo no design do seu mecanismo de sync. É lá que mora a diferença entre um app que deveria funcionar e um que funciona &lt;strong&gt;de verdade&lt;/strong&gt;.&lt;/p&gt;

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