Homeschooling e supervisão familiar em um só app: os pais acompanham tarefas, progresso de estudo e localização dos filhos, com privacidade e bateria sob controle.
01 · Contexto e problema
A Codekanox é um homeschooling com supervisão familiar em versão mobile: os pais montam o plano de estudo dos filhos, acompanham o progresso e o tempo de estudo, e veem sua localização (zonas seguras como casa ou o centro de estudo) com alertas — tudo em um app com dois papéis.
Recebi o projeto com três problemas que se chocavam: a localização em segundo plano usava GPS contínuo e drenava a bateria; o modelo de tarefas misturava o plano com o progresso (sem histórico confiável); e não havia separação real de papéis pai/filho, com risco de cruzar dados entre famílias.
O objetivo: localização em segundo plano confiável sem matar a bateria, papéis e isolamento por família server-side, e registro de estudo que não se perca sem sinal — com a privacidade de menores como base, não como anexo.
Restrições
02 · Decisões técnicas
Papéis + isolamento por família
Problema
O estado vivia disperso e sem separação de papéis; as regras não isolavam por família, então um dispositivo podia alcançar dados de outra.
Opções
Single role com flags · dois apps separados · custom claims por papel + familyId
Decisão ✓
Papéis com custom claims (pai/filho) + documento de família e vínculo por código de convite; regras Firestore que exigem o mesmo familyId. React Native + Expo (dev client) com Feature-Sliced + DDD pragmático.
Trade-off
Onboarding mais complexo (convite, verificação do vínculo), em troca de isolamento por família garantido server-side, não por convenção.
Localização em segundo plano + bateria
Problema
O GPS contínuo drenava a bateria em gama baixa, as atualizações se perdiam sem rede e os geofences eram pouco confiáveis.
Opções
GPS contínuo · geofencing nativo · significant-location-change · híbrido adaptativo
Decisão ✓
Geofencing nativo (entrar/sair de zonas seguras) + amostragem adaptativa (alta frequência só em movimento ou dentro de zonas críticas) + fila offline que sincroniza ao reconectar. expo-location + expo-task-manager para a tarefa em segundo plano.
Trade-off
Menos granularidade quando a criança está parada, em troca de −45% de bateria e zero perda de dados quando a rede cai.
Estudo/tarefas (homeschooling) + offline
Problema
O modelo misturava o plano de estudo com o progresso; o avanço se perdia sem rede e as listas longas derrubavam os FPS.
Opções
Um só modelo com flags · plano e progresso separados · fila idempotente
Decisão ✓
Separar o plano (currículo) do progresso (registro por dia, imutável), fila idempotente para o avanço, cache local (MMKV) e notificações de tarefa/zona.
Trade-off
Mais modelos e migração do schema legado, em troca de histórico confiável e avanço que não se perde offline.
Arquitetura
03 · Localização em segundo plano confiável sem drenar a bateria (e sem perder dados offline)
O desafio mais difícil foi ter localização confiável em segundo plano no Android gama baixa, onde o Doze e os battery managers agressivos matam processos e cortam o GPS — sem transformar o app num drenador de bateria nem perder posições quando a rede cai.
O GPS contínuo era inviável: descarregava a bateria e gerava escritas inúteis com a criança parada. Mas a precisão também não podia cair a ponto de falhar um alerta de «saiu da zona segura».
Solução: geofencing nativo para os eventos que importam (entrar/sair de casa ou do centro de estudo) + amostragem adaptativa (subir a frequência só em movimento ou dentro de zonas críticas) + uma fila offline com chave idempotente por marca de tempo que sincroniza com o Firestore ao reconectar. A tarefa roda com expo-task-manager para sobreviver em segundo plano.
Resultado: −45% de consumo em segundo plano, geofences confiáveis para os alertas e zero posições perdidas em quedas de rede — sem sacrificar o alerta que realmente importa.
04 · Privacidade e proteção de menores
Monitorar um menor é delicado: o design parte da minimização de dados e do consentimento. Só se coleta o necessário (localização e progresso), atado à família e nunca exposto fora dela.
Transparência: o filho sabe que está sendo acompanhado (não é espionagem encoberta); o papel e as permissões são explícitos no onboarding.
Isolamento server-side: as regras Firestore exigem o mesmo familyId para ler localização ou progresso — nenhum dispositivo alcança dados de outra família, por construção.
Retenção e exclusão: purga real dos dados ao desvincular ou excluir a conta (com dry-run prévio), cumprindo a LGPD; os dados sensíveis não são vendidos nem reutilizados para outra coisa.
Demo interativa
Em breve: execute o código e veja-o rodando ao vivo, sem instalar nada.
Editor ao vivo (DartPad) — em breve
04 · Resultados · antes / depois
05 · Retrospectiva
Definiria a estratégia de bateria/geofencing desde o dia 1: a amostragem adaptativa chegou tarde, após queixas reais de bateria; tê-la antes teria evitado retrabalho e avaliações ruins.
Fecharia antes a revisão de permissões de localização em background: as políticas da Google Play e da Apple para apps que rastreiam menores são estritas; convém projetá-las desde o início.
Manteria o isolamento por família nas regras (não no cliente), a separação plano/progresso e a transparência com o menor como regra de produto.
Código em destaque · Localização em segundo plano (amostragem adaptativa + fila offline)
A tarefa nativa (expo-task-manager) que roda em segundo plano: descarta posições irrelevantes para poupar bateria, enfileira localmente com chave idempotente e sincroniza ao reconectar — sem perder o alerta que importa.
import * as TaskManager from "expo-task-manager";
import type { LocationObject } from "expo-location";
import { isInsideCriticalZone } from "@/lib/geofences";
import { enqueue, flush } from "@/lib/location-queue";
export const BG_LOCATION = "codekanox.bg-location";
// Tarea nativa (corre aunque la app esté en segundo plano). Muestreo adaptativo
// + cola offline: no drena batería ni pierde datos cuando la red cae.
TaskManager.defineTask(BG_LOCATION, async ({ data, error }) => {
if (error || !data) return;
const { locations } = data as { locations: LocationObject[] };
const last = locations.at(-1);
if (!last) return;
const { latitude, longitude, accuracy, speed } = last.coords;
// Casi quieto y fuera de una zona crítica → no registramos: ahorra batería
// y escrituras (la posición no cambió de forma relevante).
const quieto = (speed ?? 0) < 0.6;
if (quieto && !isInsideCriticalZone(latitude, longitude)) return;
// Encolamos local (sobrevive sin red) con clave idempotente por timestamp,
// y vaciamos la cola contra Firestore si hay conexión.
await enqueue({
id: String(last.timestamp),
lat: latitude,
lng: longitude,
accuracy: accuracy ?? null,
at: last.timestamp,
});
await flush(); // si falla por red, queda en cola para el próximo ciclo
});Em entrevista se nota: trabalho em segundo plano real, amostragem adaptativa por bateria, fila offline com idempotência (sem duplicados nem perdas) e a decisão do que NÃO registrar — critério, não só código.