Las otras notas de la serie enseñan conceptos con ejemplos. Esta cuenta cómo encajaron al construir ResistGate.
Es un relato histórico del diseño descrito en mayo de 2026, no una especificación exacta de todas las versiones posteriores.
El problema que quería resolver
Abría Reddit y YouTube por reflejo: entre líneas de código, pruebas o compilaciones. La acción llegaba antes de pensar si quería hacerlo.
Probé bloqueadores como Cold Turkey, LeechBlock y StayFocusd. Los bloqueos absolutos me resultaban demasiado rígidos y acababa desactivándolos o buscando otra salida.
Quería introducir un esfuerzo que interrumpiera el reflejo y aun así permitiera tomar un descanso deliberado.
En el diseño descrito aquí, ResistGate pide un reto antes de entrar. Al completarlo, concede quince minutos de acceso; después vuelve el bloqueo.
Cómo organicé la arquitectura
Usé tres de los componentes de una extensión:
El worker coordina el estado
Se ocupa de:
- Observar navegaciones mediante
chrome.webNavigation.onBeforeNavigate. - Gestionar sitios bloqueados y ventanas de acceso.
- Programar vencimientos con
chrome.alarms. - Coordinar almacenamiento y mensajes.
La decisión esencial fue no depender de variables globales para conservar el estado. Las listas, sesiones y marcas de tiempo viven en chrome.storage. Cuando el worker arranca, vuelve a leer lo necesario.
Es el patrón de la guía de almacenamiento, que aprendí después de encontrar el fallo en la práctica.
El script de contenido presenta el reto
Cuando corresponde bloquear una página, la extensión muestra su interfaz de reto:
- Sustituye el contenido visible por los controles.
- Presenta el nivel de dificultad elegido.
- Comprueba la escritura carácter por carácter.
- Comunica la finalización mediante mensajes.
El script vive por defecto en un mundo aislado: sus variables JavaScript están separadas de las de la página. El DOM sigue siendo compartido y la web puede modificarlo. Ese aislamiento no convierte el reto en una barrera imposible de alterar.
El popup reúne los controles
Incluye:
- Lista de sitios bloqueados.
- Dificultad del reto.
- Estadísticas y puntuación semanal.
- Modo de compromiso para bloquear cambios de ajustes durante un periodo.
Lo construí con TypeScript sin React. Al principio tenía pocas actualizaciones de DOM. El hook de la guía de React sería otra opción, pero entonces preferí una implementación pequeña.
Las decisiones que costaron más
Cómo responder a las navegaciones
Consideré dos enfoques:
chrome.webNavigation.onBeforeNavigate: observar el comienzo de la navegación y coordinar una respuesta desde el worker. El evento no es por sí mismo una API de bloqueo síncrono.
chrome.declarativeNetRequest: aplicar reglas declarativas de red. También admite reglas dinámicas; no se limita a listas estáticas.
El diseño inicial usó el primero para coordinar la lista, los retos y las ventanas de acceso. Esa elección requiere gestionar el tiempo entre observar y actuar.
El acceso amplio a sitios responde a que el usuario puede bloquear dominios que no conozco de antemano. El permiso webNavigation y el acceso de host son capacidades distintas y deben declararse según las APIs utilizadas.
La guía de permisos explica cómo justificar ese alcance y describir el tratamiento real de datos.
Validar el reto mientras se escribe
En el modo difícil del diseño original hay un pasaje largo que debe escribirse con precisión y sin pegarlo.
Comparar solo al final resultaba frustrante: podías escribir quinientos caracteres para descubrir un error al principio.
Opté por comprobar cada carácter y marcar el error de inmediato, sin avanzar mientras no coincidiera:
// Simplified version of the validation logic
function validateInput(input: string, expected: string): ValidationResult {
for (let i = 0; i < input.length; i++) {
if (input[i] !== expected[i]) {
return { valid: false, errorPosition: i, correctCount: i };
}
}
return {
valid: input.length === expected.length,
errorPosition: null,
correctCount: input.length,
};
}
Introducir fricción al pegar texto
Bloquear el pegado ordinario evita saltarse el esfuerzo con una sola acción:
challengeInput.addEventListener("paste", (e) => e.preventDefault());
challengeInput.addEventListener("drop", (e) => e.preventDefault());
También probé una comprobación de intervalos muy cortos entre caracteres, alrededor de veinte milisegundos, para detectar ciertos automatismos. Es una heurística de fricción; no demuestra si una persona escribe ni evita todas las formas de automatización.
Gestionar las ventanas de acceso
Al completar un reto:
- Guardo
{ domain, expiresAt }enchrome.storage.local. - Programo una alarma para el vencimiento.
- Compruebo esa marca al navegar al dominio.
- Al vencer, retiro el acceso y restauro el bloqueo.
El estado persistido y la reconciliación al despertar permiten recuperarse de la terminación del worker. Un setTimeout en memoria no basta.
El modo de compromiso
Bloquea cambios dentro de la interfaz de la extensión durante el periodo elegido, de una a veinticuatro horas en este diseño. Chrome sigue permitiendo desactivar o desinstalar la extensión.
Casos que tuve que contemplar:
- Actualizaciones: el estado debe poder reconstruirse tras reiniciar la extensión.
- Cambios del reloj: guardar duración e inicio ayuda a restaurar la sesión, pero no impide manipular el mismo reloj que usas para calcular el vencimiento. Hace falta una estrategia adicional si quieres detectar esos cambios.
- Desinstalación: no intento convertir la extensión en un control imposible de eludir. Su propósito es introducir fricción voluntaria.
La puntuación de disciplina
La fórmula del diseño original era retos completados divididos entre retos completados más desactivaciones registradas desde la propia interfaz. El dato se guardaba localmente.
Era una decisión de producto: hacer visible el comportamiento para que el usuario pensara antes de quitar el bloqueo. No equivale a observar todas las desactivaciones posibles desde Chrome.
Casos que rompieron el primer diseño
Páginas internas. La inyección no está permitida en páginas restringidas, como las de chrome://. Hay que revisar la URL y manejar el error.
Varias pestañas del mismo sitio. Un permiso almacenado solo por dominio se aplica a todas las pestañas de ese dominio. Si quieres un reto independiente por pestaña, debes guardar y comprobar también su identificador. { domain, expiresAt } no implementa por sí solo esa política.
Cadenas de redirecciones. Una URL corta puede acabar en otro dominio. Observar la navegación inicial no basta para conocer siempre el destino final; en el diseño descrito también revisaba onCompleted.
Chrome móvil. El Chrome móvil habitual no carga estas extensiones de escritorio. Conviene explicarlo cuando alguien intenta instalarlas desde el teléfono.
Qué cambiaría al empezar de nuevo
Evaluaría React antes. El popup creció y la manipulación manual del DOM se volvió difícil de mantener. La configuración con React habría sido una alternativa razonable al llegar a ese punto.
Mejoraría la introducción al producto. Un reto interactivo al instalar puede explicar mejor el mecanismo que una página de bienvenida llena de texto.
Definiría antes qué medir. Las métricas locales y, cuando corresponda, información agregada con consentimiento pueden ayudar a evaluar el producto sin introducir vigilancia innecesaria.
El tamaño de aquella primera versión
- Unas tres semanas de noches y fines de semana.
- Aproximadamente 2400 líneas de TypeScript.
- Unas 800 en el worker.
- Unas 1000 en la interfaz del reto.
- Unas 600 en el popup.
- Dieciocho archivos.
Son cifras del trabajo descrito entonces. La complejidad estaba en el estado y los casos de borde, no en el volumen.
Si quieres construir algo parecido
- Empieza por el tutorial de TypeScript.
- Comprende la arquitectura de Manifest V3.
- Prepara el lanzamiento con la guía de publicación.
También puedes probar ResistGate y ver cómo introduce ese paso deliberado antes de una distracción.
Esta nota forma parte de la serie Extensiones de Chrome: de cero a producto. Las demás guías explican patrones que aprendí al construir este producto.
Un pequeño voto de confianza
¿Valió la pena leer esta nota?