Homeschooling y supervisión familiar en una sola app: los padres acompañan tareas, progreso de estudio y ubicación de sus hijos, con privacidad y batería bajo control.
01 · Contexto y problema
Codekanox es un homeschooling con supervisión familiar en versión móvil: los padres arman el plan de estudio de sus hijos, siguen el progreso y el tiempo de estudio, y ven su ubicación (zonas seguras como casa o el centro de estudio) con alertas — todo en una app con dos roles.
Recibí el proyecto con tres problemas que chocaban: la ubicación en segundo plano usaba GPS continuo y drenaba la batería; el modelo de tareas mezclaba el plan con el progreso (sin histórico fiable); y no había separación real de roles padre/hijo, con riesgo de cruzar datos entre familias.
El objetivo: ubicación en segundo plano fiable sin matar la batería, roles y aislamiento por familia server-side, y registro de estudio que no se pierda sin señal — con la privacidad de menores como base, no como anexo.
Restricciones
02 · Decisiones técnicas
Roles + aislamiento por familia
Problema
El estado vivía disperso y sin separación de roles; las reglas no aislaban por familia, así que un dispositivo podía alcanzar datos de otra.
Opciones
Single role con flags · dos apps separadas · custom claims por rol + familyId
Decisión ✓
Roles con custom claims (padre/hijo) + documento de familia y vínculo por código de invitación; reglas Firestore que exigen el mismo familyId. React Native + Expo (dev client) con Feature-Sliced + DDD pragmático.
Trade-off
Onboarding más complejo (invitación, verificación del vínculo), a cambio de aislamiento por familia garantizado server-side, no por convención.
Ubicación en segundo plano + batería
Problema
El GPS continuo drenaba la batería en gama baja, las actualizaciones se perdían sin red y los geofences eran poco fiables.
Opciones
GPS continuo · geofencing nativo · significant-location-change · híbrido adaptativo
Decisión ✓
Geofencing nativo (entrar/salir de zonas seguras) + muestreo adaptativo (alta frecuencia solo en movimiento o dentro de zonas críticas) + cola offline que sincroniza al reconectar. expo-location + expo-task-manager para la tarea en segundo plano.
Trade-off
Menos granularidad cuando el niño está quieto, a cambio de −45 % de batería y cero pérdida de datos cuando la red cae.
Estudio/tareas (homeschooling) + offline
Problema
El modelo mezclaba el plan de estudio con el progreso; el avance se perdía sin red y las listas largas tiraban los FPS.
Opciones
Un solo modelo con flags · plan y progreso separados · cola idempotente
Decisión ✓
Separar el plan (currículo) del progreso (registro por día, inmutable), cola idempotente para el avance, cache local (MMKV) y notificaciones de tarea/zona.
Trade-off
Más modelos y migración del schema legacy, a cambio de histórico fiable y avance que no se pierde offline.
Arquitectura
03 · Ubicación en segundo plano fiable sin drenar la batería (y sin perder datos offline)
El reto más difícil fue tener ubicación confiable en segundo plano en Android gama baja, donde Doze y los battery managers agresivos matan procesos y recortan el GPS — sin convertir la app en un drenador de batería ni perder posiciones cuando la red cae.
El GPS continuo era inviable: descargaba la batería y generaba escrituras inútiles con el niño quieto. Pero la precisión tampoco podía bajar tanto como para fallar una alerta de «salió de la zona segura».
Solución: geofencing nativo para los eventos que importan (entrar/salir de casa o del centro de estudio) + muestreo adaptativo (subir la frecuencia solo en movimiento o dentro de zonas críticas) + una cola offline con clave idempotente por marca de tiempo que sincroniza contra Firestore al reconectar. La tarea corre con expo-task-manager para sobrevivir en segundo plano.
Resultado: −45 % de consumo en segundo plano, geofences fiables para las alertas y cero posiciones perdidas en cortes de red — sin sacrificar la alerta que de verdad importa.
04 · Privacidad y protección de menores
Monitorear a un menor es delicado: el diseño parte de la minimización de datos y el consentimiento. Solo se recoge lo necesario (ubicación y progreso), atado a la familia y nunca expuesto fuera de ella.
Transparencia: el hijo sabe que está siendo acompañado (no es espionaje encubierto); el rol y los permisos son explícitos en el onboarding.
Aislamiento server-side: las reglas Firestore exigen el mismo familyId para leer ubicación o progreso — ningún dispositivo alcanza datos de otra familia, por construcción.
Retención y borrado: purga real de datos al desvincular o eliminar la cuenta (con dry-run previo), cumpliendo LGPD; los datos sensibles no se venden ni se reutilizan para otra cosa.
Demo interactiva
Próximamente: ejecuta el código y míralo correr en vivo, sin instalar nada.
Editor en vivo (DartPad) — próximamente
04 · Resultados · antes / después
05 · Retrospectiva
Definiría la estrategia de batería/geofencing desde el día 1: el muestreo adaptativo llegó tarde, tras quejas reales de batería; tenerlo antes habría evitado retrabajo y malas reseñas.
Cerraría antes la revisión de permisos de ubicación en background: las políticas de Google Play y Apple para apps que rastrean menores son estrictas; conviene diseñarlas desde el inicio.
Conservaría el aislamiento por familia en reglas (no en cliente), la separación plan/progreso y la transparencia con el menor como regla de producto.
Código destacado · Ubicación en segundo plano (muestreo adaptativo + cola offline)
La tarea nativa (expo-task-manager) que corre en segundo plano: descarta posiciones irrelevantes para ahorrar batería, encola localmente con clave idempotente y sincroniza al reconectar — sin perder la 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
});En entrevista se nota: trabajo en segundo plano real, muestreo adaptativo por batería, cola offline con idempotencia (sin duplicados ni pérdidas) y la decisión de qué NO registrar — criterio, no solo código.