// RED SOCIAL · ACUARIOFILIA · PRODUCTO EN PRODUCCIÓN

ankily

El diario de tu acuario y la gente que lo entiende. Una plataforma social y técnica, en 8 idiomas, donde cada usuario registra sus tanques, mide su agua y comparte con acuaristas de todo el mundo.

Tankily en escritorio y móvil: diario de un tanque de arrecife y ficha de especie
1.847Fichas de especies
8Idiomas completos
16k+URLs indexables
105Políticas RLS
7Calculadoras
1Persona detrás

EL PRODUCTO

Los acuaristas apuntan sus mediciones en libretas, preguntan en foros de hace quince años y compran peces sin saber si van a convivir. Tankily une todo eso en un solo sitio que gira alrededor del tanque: cada acuario tiene su agua, sus habitantes, su diario y sus recordatorios. Alrededor nace la comunidad: muro, foro, compraventa entre aficionados, directorio de tiendas y WikiTank, una enciclopedia con fichas técnicas que alimenta todo lo demás. Lo he concebido, diseñado, programado y lanzado yo solo, y hoy está en producción con usuarios reales, plan de pago y campaña en redes.

INFRAESTRUCTURA TÉCNICA

  • FRONT-END: Next.js 16 (App Router) y React 19 con export estático: miles de páginas pregeneradas, servidas como HTML plano.
  • BACKEND: Supabase: Postgres con RLS en todas las tablas, Auth (email, Google y Apple), Storage y Realtime.
  • SERVIDOR: Edge Functions en Deno (avisos, push, Stripe, traducción) y tareas programadas con pg_cron.
  • PAGOS Y DATOS: Stripe para el plan Pro, Umami para analítica sin cookies, IndexNow y sitemaps por idioma.

01 / EL CORAZÓN DEL PRODUCTO

El diario del tanque

Cada acuario es una ficha viva: volumen, días en marcha, parámetros del agua con gráficas, habitantes enlazados a WikiTank, diario de mantenimiento y publicaciones. Los tanques públicos se pueden seguir, y cada uno es también una página indexable que trae visitas desde buscadores.

  • Mantenimiento: tareas periódicas por tanque; el cambio de agua es una tarea virtual que se calcula a partir del último evento del diario. Un tanque nuevo nace con recordatorios sugeridos según su tipo.
  • Panel «Hoy»: la portada reúne lo pendiente de todos los tanques del usuario, con avisos por email y notificaciones push.
  • Unidades: en la base de datos todo es métrico; useUnidades() convierte al sistema imperial al mostrar y al guardar.
tankily.com/es/tanque/arrecife-del-salon
Tankily - ficha pública de un tanque de arrecife con sus parámetros

// Pantalla: tanque público con mediciones de agua de arrecife.

02 / LÓGICA DE DOMINIO

Motor de compatibilidad

La pregunta que más se repite en acuariofilia es «¿pueden convivir?». La respuesta sale de un módulo de funciones puras, con pruebas, que cruza las fichas de las especies entre sí y con la última medición real del tanque.

  • Qué detecta: agua dulce o marina equivocada, especies no aptas para arrecife, rangos sin solape de temperatura, pH, GH y salinidad, litros insuficientes, grupos demasiado pequeños y agresivos junto a pacíficos.
  • Dónde vive: en la pestaña Habitantes, como vista previa al añadir una especie y como calculadora pública sin registro, que es también la mejor puerta de entrada al producto.
  • Embudo: la calculadora guarda las especies elegidas como «tanque pendiente» y las añade solas cuando el visitante crea su cuenta.
tankily.com/es/herramientas/compatibilidad
Tankily - calculadora de compatibilidad entre especies

// Pantalla: betta y gamba cherry en 60 litros, con el agua ideal para ambos.

03 / CONTENIDO COMO MOTOR

WikiTank y 1.847 especies

La enciclopedia es el sistema nervioso de Tankily: sus fichas técnicas alimentan el autocompletado de habitantes, el motor de compatibilidad, las calculadoras y el SEO. En el build se leen los Markdown, se unen duplicados por nombre científico y se genera un índice compartido por todo el producto.

  • Datos contrastados: nombres y familias validados con GBIF; temperatura, pH y tamaño cotejados con FishBase como referencia.
  • Fotos con licencia: imágenes de Wikimedia Commons con licencia libre y crédito visible, revisadas a ojo en hojas de contactos para descartar ejemplares de museo y dibujos.
  • 8 idiomas: fichas y artículos traducidos pieza a pieza, con canónica y hreflang propios; una ficha solo se indexa en un idioma cuando está traducida entera.
tankily.com/es/wiki/especies
Tankily - catálogo de especies de WikiTank con filtros

// Pantalla: catálogo con filtros por tipo, agua, dificultad y aptitud para arrecife.

04 / MOBILE FIRST · PWA

Pensado para el móvil, junto al acuario

Tres de cada cuatro visitas llegan desde el teléfono, normalmente con el test de agua en la mano. La interfaz cambia la barra lateral por una barra inferior de cristal, prioriza las cifras grandes y se instala como app (PWA) con iconos propios, modo sin conexión y notificaciones push.

  • Navegadores de apps: detecta el navegador interno de Instagram y Facebook, donde Google bloquea el acceso con su cuenta, y guía al usuario para abrir Chrome o Safari.
  • Rendimiento: la refracción de cristal en SVG se reserva para modales y fichas; el resto usa cristal ligero para mantener el scroll fluido en móviles modestos.
Tankily - portada en móvil

// Portada

Tankily - ficha de especie en móvil

// Ficha de especie

Tankily - tanque en móvil

// Tanque

05 / COMUNIDAD SIN FRONTERAS

Foro, muro y mercado

Un acuarista polaco puede responder al hilo de uno brasileño. Cada publicación guarda su idioma y, si no coincide con el del lector, se traduce sola con «Traducido del francés · Ver original».

  • Traducción por capas: caché local y tabla compartida, luego el traductor integrado de Chrome y, en último caso, una Edge Function que solo traduce filas existentes. Etiquetas, menciones y enlaces nunca se tocan.
  • Antispam en la base de datos: un trigger genérico limitar_frecuencia() limita publicaciones, mensajes, anuncios y reseñas por usuario y periodo.
  • Campos blindados: permisos por columna para que nadie pueda marcarse como admin, verificar su tienda o destacar su anuncio desde el cliente.
tankily.com/es/foro
Tankily - foro por categorías

// Pantalla: foro organizado por tipo de acuario y ayuda.

06 / INGENIERÍA DE ENTREGA

Del commit a producción, sin servidor que mantener

El export estático multiplicaba las páginas por ocho idiomas y el primer build pesó 5,6 GB en 184.000 archivos: el despliegue por FTP no terminaba nunca. Cargar el diccionario por idioma en lugar de incrustarlo en cada página y cambiar a rsync por SSH lo dejó en menos de la mitad, con despliegues incrementales en minutos.

01 · PUSHGitHub

Push a main lanza el flujo de GitHub Actions.

02 · BUILDNext.js export

Índice de especies, ads.txt, sitemaps y páginas por idioma.

03 · DEPLOYrsync / SSH

Solo sube lo que cambia, con clave dedicada.

04 · NOCHEBuild nocturno

Regenera tanques, hilos y anuncios públicos y avisa con IndexNow.

07 / ARQUITECTURA

Cómo encajan las piezas

  • 43migraciones SQL
  • 32tablas
  • 105políticas RLS
  • 32pruebas con node --test
  • 4Edge Functions
  • 8idiomas
Navegadorlo que recibe el usuario
HTML pregeneradomás de 16.000 URLs indexables
React 19hidrata y habla con Supabase
PWAinstalable, sin conexión y push
Traductor de Chromesegunda capa de traducción
Supabasedatos y reglas
Postgres con RLSen todas las tablas
Authemail, Google y Apple
Storagefotos y archivos de los usuarios
Realtimemuro y usuarios en línea
Servidorsin servidor que mantener
Edge Functions (Deno)avisos, push, Stripe y traducción
pg_crontareas programadas
Stripeplan Pro
Entregadel commit a producción
GitHub Actionsbuild y build nocturno
rsync por SSHsolo lo que cambia
HostingerHTML estático
IndexNowaviso a buscadores

// Decisiones técnicas y por qué

ProblemaMiles de páginas en 8 idiomas en un hosting compartido.
DecisiónNext.js con export estático: cada página sale como HTML y los datos privados llegan desde el navegador directamente a Supabase.
En vez deUn servidor Node propio, que una sola persona tendría que mantener y pagar.
ProblemaEl navegador habla directo con la base de datos.
DecisiónLas reglas viven en Postgres: RLS en todas las tablas, permisos por columna y triggers para lo que no se puede dejar al cliente.
En vez deUna API intermedia que duplicaría cada regla.
ProblemaSpam en foro, mensajes y anuncios.
DecisiónUn único trigger genérico, limitar_frecuencia(), con índices por autor y fecha para que el recuento no recorra la tabla.
En vez deUn servicio externo de límites o Redis.
ProblemaTraducir publicaciones entre 8 idiomas cuesta dinero.
DecisiónTraducción por capas: caché local, tabla compartida, traductor integrado de Chrome y, solo al final, una Edge Function.
En vez deMandar cada texto a una API de pago.
ProblemaLa compatibilidad entre especies es la lógica más delicada.
DecisiónFunciones puras en src/lib, sin React, cubiertas con pruebas de node --test.
En vez deLógica repartida dentro de los componentes, imposible de probar.

08 / EN EL CÓDIGO

Fragmentos reales del repositorio

Motor de compatibilidad

Para saber si varias especies pueden compartir agua, se cruzan sus rangos de temperatura, pH, GH y salinidad. Si la intersección está vacía, hay conflicto. Cada parámetro tiene un margen de tolerancia para no avisar por diferencias que no importan.

Es una función pura: recibe datos y devuelve avisos, así que se prueba sin navegador ni base de datos.

src/lib/compatibilidad.jsJavaScript
// Rango común de una lista de rangos [min, max]; null si no se solapan
function interseccion(rangos) {
  if (!rangos.length) return undefined;
  const min = Math.max(...rangos.map((r) => r[0]));
  const max = Math.min(...rangos.map((r) => r[1]));
  return min <= max ? [min, max] : null;
}

const PARAMS = [
  // margen: tolerancia de medida para no avisar por diferencias que no importan
  { clave: 't', parametro: 'temperatura', margen: 0.5 },
  { clave: 'ph', parametro: 'ph', margen: 0.1 },
  { clave: 'gh', parametro: 'gh', soloDulce: true, margen: 1 },
  { clave: 'sal', parametro: 'salinidad', soloMarino: true,
    convertir: densidadAPpt, margen: 2 },
];
Antispam en la base de datos

Un solo trigger sirve para ocho tablas: recibe por argumentos qué columna identifica al autor, cuántas inserciones se permiten y en qué periodo. Si se supera, Postgres rechaza la inserción con un código que la interfaz traduce a un mensaje claro.

supabase/migrations/…_limite_frecuencia.sqlSQL
create or replace function public.limitar_frecuencia()
returns trigger language plpgsql security definer set search_path = '' as $$
declare
  columna text := tg_argv[0];               -- columna con el autor
  maximo int := tg_argv[1]::int;            -- inserciones permitidas…
  periodo interval := tg_argv[2]::interval; -- …en este periodo
  autor uuid := (to_jsonb(new) ->> columna)::uuid;
  recientes int;
begin
  if autor is null or (select auth.uid()) is null or public.es_admin() then return new; end if;
  execute format('select count(*) from %I.%I where %I = $1 and creado_en > now() - $2',
    tg_table_schema, tg_table_name, columna) into recientes using autor, periodo;
  if recientes >= maximo then
    raise exception 'LIMITE_FRECUENCIA' using errcode = 'P0001';
  end if;
  return new;
end $$;

create trigger limite_mensajes before insert on public.mensajes
  for each row execute function public.limitar_frecuencia('emisor_id', '60', '10 minutes');

RESUMEN DEL IMPACTO

Tankily es un producto completo construido por una sola persona: investigación del nicho, marca, diseño de interfaz, modelo de datos con seguridad a nivel de fila, lógica de dominio con pruebas, internacionalización real en ocho idiomas, SEO técnico a escala de miles de páginas, pagos, notificaciones y una campaña de contenido en redes. Es la demostración más clara de cómo trabajo: entender un problema concreto y llevarlo hasta algo que funciona en producción.