JobBot
Un SaaS de búsqueda laboral que junta ofertas de varias fuentes, las filtra por tu perfil y te avisa por Telegram para que postules en las primeras horas. Lo construí solo, de punta a punta: API, dashboard, bot, pagos, CI y deploy.
A job-search SaaS that pulls openings from several sources, filters them against your profile and pings you on Telegram so you apply within the first hours. I built it solo, end to end: API, dashboard, bot, payments, CI and deploy.
El problema
The problem
Buscar trabajo es un desgaste con tres fugas concretas: se pierden oportunidades (decenas de ofertas nuevas por semana repartidas en sitios distintos), se aplica tarde (las primeras 24 horas deciden la mayoría de las entrevistas) y no hay seguimiento (horas refrescando LinkedIn sin saber qué funcionó). Lo viví buscando mi propia primera experiencia en tecnología.
Job hunting leaks in three concrete places: you miss openings (dozens of new posts a week spread across different sites), you apply late (the first 24 hours decide most interviews) and there's no follow-up (hours refreshing LinkedIn with no idea what worked). I lived it looking for my own first tech role.
Qué hace hoy
What it does today
- Busca en varias fuentes (APIs públicas de empleo remoto, Google Jobs y LinkedIn con respaldo por RSS) y deduplica.
- Alertas por Telegram con las ofertas que coinciden con tus preferencias y horarios.
- Dashboard con login real, historial y planes (Free, Starter, Pro y Premium).
- CV Suite: análisis de CV con IA, cobrado con un sistema de créditos.
- Checkout funcionando con Stripe y MercadoPago.
- Searches several sources (public remote-job APIs, Google Jobs, and LinkedIn with an RSS fallback) and deduplicates.
- Telegram alerts for openings that match your preferences and schedule.
- Dashboard with real login, history and plans (Free, Starter, Pro and Premium).
- CV Suite: AI CV analysis, billed through a credits system.
- Working checkout with Stripe and MercadoPago.
Arquitectura
Architecture
La landing vende y deriva tráfico; el producto autenticado vive en el dashboard. Son dos deploys separados a propósito.
The landing sells and routes traffic; the authenticated product lives in the dashboard. Two separate deploys, on purpose.
Decisiones técnicas
Technical decisions
Separar landing y app
Separate landing and app
La landing es HTML estático: carga instantánea y cambios de copy sin tocar el producto. El dashboard es Next.js con auth. Así un error de marketing no puede romper el login, y viceversa.
The landing is static HTML: instant load and copy changes without touching the product. The dashboard is Next.js with auth. A marketing mistake can't break login, and vice versa.
Cookies httpOnly en vez de tokens en localStorage
httpOnly cookies instead of tokens in localStorage
Un token en localStorage lo lee cualquier script que se cuele en la página. En una cookie httpOnly, JavaScript no puede leerlo. Sumé refresh tokens agrupados en "familias" para detectar si alguien reutiliza uno robado y revocar toda la sesión.
A token in localStorage can be read by any script that sneaks into the page; an httpOnly cookie can't be read by JavaScript at all. I added refresh tokens grouped into "families" so reuse of a stolen one is detected and the whole session revoked.
Muchas fuentes, ninguna imprescindible
Many sources, none essential
Los sitios de empleo cambian o bloquean sin aviso. Cada fuente es independiente, y LinkedIn tiene un camino de respaldo por RSS: si una se cae, las alertas siguen llegando con las demás.
Job sites change or block without warning. Each source is independent, and LinkedIn has an RSS fallback path: if one breaks, alerts keep arriving from the rest.
Dos pasarelas de pago
Two payment gateways
Stripe para tarjetas internacionales y MercadoPago porque es lo que usa la gente en Argentina. Las dos confirman el pago a la API por webhook, así que el plan se activa aunque el usuario cierre la pestaña.
Stripe for international cards and MercadoPago because it's what people actually use in Argentina. Both confirm payment to the API via webhook, so the plan activates even if the user closes the tab.
Créditos para lo que cuesta IA
Credits for anything that costs AI
El análisis de CV llama a un modelo de IA con costo por uso. Un sistema de créditos evita que un plan fijo termine regalando lo que cuesta plata.
CV analysis calls a pay-per-use AI model. A credits system keeps a flat plan from giving away what costs money.
CI con base de datos real
CI against a real database
Los tests corren en GitHub Actions contra PostgreSQL y Redis reales, no contra mocks. Si una migración rompe algo, falla el pipeline, no el usuario.
Tests run in GitHub Actions against real PostgreSQL and Redis, not mocks. If a migration breaks something, the pipeline fails, not the user.
Qué salió mal (y cómo lo encontré)
What went wrong (and how I found it)
Antes del lanzamiento hice una revisión de código y una auditoría de seguridad sobre mi propio proyecto (los informes están en el repo). La nota global fue 72/100. Estos son hallazgos reales:
Before launch I ran a code review and a security audit on my own project (the reports are in the repo). The overall score was 72/100. These are real findings:
Comparación del login de Telegram vulnerable a timing attacksTelegram login check open to timing attackscorregidofixed
Comparar un hash con == tarda distinto según cuántos caracteres coinciden, y eso filtra información. Detectado el 2 de abril; el 9 pasó a hmac.compare_digest, que compara en tiempo constante.
Comparing a hash with == takes a different time depending on how many characters match, which leaks information. Found on April 2; by April 9 it used hmac.compare_digest, which runs in constant time.
La detección de tokens robados nunca podía activarseStolen-token detection could never firecorregidofixed
Cada renovación de sesión creaba una "familia" de tokens nueva en vez de continuar la anterior. Como la detección de reuso compara dentro de una misma familia, un token robado nunca iba a disparar la alarma. Ahora la rotación mantiene la familia y guarda qué token reemplazó a cuál.
Every session refresh started a brand-new token "family" instead of continuing the old one. Since reuse detection works within a family, a stolen token could never trip it. Rotation now keeps the family and records which token replaced which.
Dos funciones distintas actualizaban el plan del usuarioTwo different functions updated a user's plancorregidofixed
Había una update_user_plan duplicada, así que según qué camino tomaba el pago, los límites del plan quedaban distintos. Dejé una sola y agregué un test que cubre los límites comerciales.
A duplicated update_user_plan meant plan limits depended on which payment path ran. I kept one and added a test covering the commercial limits.
La landing prometía cosas que el producto no medíaThe landing promised things the product didn't measurecorregidofixed
Armé una matriz que contrasta cada frase de la landing con el código de la API. Decía "15+ portales" (eran 6), mostraba "42 empleos esta semana" (dato de demo) y "Top 10% de usuarios" (nada lo medía). Los corregí o los saqué, y los precios quedaron tomados de la API como única fuente.
I built a matrix checking every landing claim against the API code. It said "15+ job boards" (there were 6), showed "42 jobs this week" (demo data) and "top 10% of users" (nothing measured it). I corrected or removed them, and prices now come from the API as the single source.
Un secreto mal configurado se reporta como error 500A misconfigured secret surfaces as a 500 errorpendienteopen
Si falta la clave para firmar los tokens, la API debería negarse a arrancar. Hoy arranca igual y recién falla cuando alguien intenta iniciar sesión. Es el próximo arreglo; lo dejo acá porque no quiero mostrar solo lo que salió bien.
If the token signing key is missing, the API should refuse to start. Today it starts anyway and only fails when someone tries to log in. It's the next fix; I'm listing it because I don't want to show only what went right.
Qué aprendí
What I learned
- Auditar lo propio con la misma dureza que lo ajeno. Ninguno de los bugs de arriba aparecía usando la app; aparecieron leyendo el código con otra mirada.
- Una sola fuente de verdad. Los planes duplicados y las promesas infladas son el mismo error: un dato definido en dos lugares, o en ninguno.
- Lo que puede fallar en silencio, falla en silencio. Ahora prefiero que un sistema se niegue a arrancar antes que parecer que funciona.
- La IA acelera, el criterio es mío. Usé asistentes para escribir y revisar, pero decidir qué construir y qué hallazgo importaba fue trabajo mío.
- Audit your own work as harshly as anyone else's. None of the bugs above showed up while using the app; they showed up reading the code with fresh eyes.
- One source of truth. Duplicated plan logic and inflated claims are the same mistake: data defined in two places, or in none.
- What can fail silently, will. I now prefer a system that refuses to start over one that pretends to work.
- AI speeds things up; judgment stays mine. I used assistants to write and review, but deciding what to build and which finding mattered was my job.