Cómo construí Piso Propio

La historia técnica detrás de Piso Propio: unificar decenas de fuentes oficiales de vivienda protegida en España con scraping, extracción con IA y un motor de elegibilidad.


Mi novia es de Esplugues de Llobregat, al lado de Barcelona. Hace unos meses se puso a ver cómo comprar su primera casa, y la vivienda protegida era la mejor opción para ella. Lo difícil fue entender la burocracia: ¿califico?, ¿cuánto sale?, ¿cuándo se abre la convocatoria y cuándo cierra?

El problema no es que la información no exista. Existe, pero está repartida entre decenas de webs oficiales, registros y PDFs que parecen hechos para que nadie los lea. Cada ayuntamiento publica distinto, cada operador metropolitano tiene su propio portal, y los plazos se vencen mientras vos tratás de entender en qué quedó todo.

Así que me puse a construir Piso Propio: una herramienta para el mercado de vivienda protegida en España que te dice para qué ofertas calificás, cuánto cuestan, cuándo cierran y qué chances reales tenés de que te toque. Por ahora cubre 13 comunidades, junta 22 fuentes oficiales y vigila 228 ofertas.

Te cuento cómo está hecha por dentro.

El problema no es la app, son los datos

Armar el frontend fue lo menos difícil. Lo difícil fue agarrar todo ese desorden de información oficial y convertirlo en algo que se pueda consultar.

En España la vivienda protegida no tiene un único lugar donde mirar. Están los registros, como el RSHPOB de Barcelona o el Registre de Sol·licitants de Catalunya. Están los operadores metropolitanos, como AMB/IMPSOL y Habitatge Metròpolis Barcelona. Están las oficinas de cada municipio. Y arriba de todo están los boletines oficiales (BOJA, DOCM, BOCM, DOG, BOIB, BOPV), donde las convocatorias aparecen perdidas entre mil cosas más.

En total tengo definidas unas 39 fuentes, con 22 funcionando hoy. Ninguna se parece a la otra. Empecé solo con Barcelona y su área metropolitana, y de a poco fui sumando hasta llegar a 13 comunidades. Cada comunidad nueva es, básicamente, empezar de cero con otro formato.

Todo eso pasa por un pipeline de seis etapas sobre BullMQ, donde cada etapa encola la siguiente:

flowchart LR
    F([fetch]) --> P([parse])
    P --> N([normalize])
    N --> M([match])
    M --> A([notify])

fetch baja el contenido crudo, parse lo interpreta, normalize lo lleva a un modelo común, match lo cruza con tu perfil y notify te avisa de plazos y cambios. Hay una sexta cola, maintenance, para lo aburrido: reintentos, limpieza y revisar qué fuentes dejaron de contestar.

Para el usuario, lo que importa son las dos últimas. match compara cada oferta con tu situación (ingresos, cuántos son en casa, en qué municipios buscás) y notify te avisa antes de que se venza un plazo. Suena obvio, pero ahí está la mitad del valor: casi nadie se queda sin vivienda por no calificar, se queda afuera por enterarse tarde.

La mayoría de las fuentes las resuelvo con lo más barato que hay: un fetch por HTTP y Cheerio para leer el HTML. Playwright lo dejo solo para las que bloquean bots en serio, como AMB/IMPSOL, porque levantar un navegador headless para cada cosa es carísimo. Los PDFs los leo con unpdf.

La etapa que no esperaba que me diera tanto trabajo fue normalize. La misma promoción aparece en tres fuentes con tres nombres distintos, y hay que darse cuenta de que son la misma. Lo resuelvo con pg_trgm, la extensión de PostgreSQL que compara strings por similitud, más una clave canónica que arma el código de municipio con un slug del título. Y cuando el HTML del listado dice una cosa y el PDF de las bases dice otra, le creo al PDF: las bases son el documento legal, el listado es apenas una pista.

Mi parte favorita de todo esto es una ironía. La web que arrancó el proyecto, esplugues.cat, resultó ser la más rota de todas: tiene la cadena de certificados TLS incompleta, así que la conexión se cae sola. Tuve que activar insecureTls para esa fuente en particular. La ciudad de mi novia, la que originó todo, fue la más difícil de scrapear.

La IA, donde de verdad suma

Una convocatoria de vivienda protegida suele ser un PDF de varias páginas, en catalán o castellano legal, con topes de ingresos, cupos por colectivo, plazos y la lista de papeles que tenés que presentar. Sacar eso con expresiones regulares es pelear contra un molino: armás el regex para un ayuntamiento y se rompe con el siguiente.

Acá Claude hace algo que ninguna heurística mía pudo igualar: uso el modelo claude-opus-4-8 para sacar los campos estructurados de cada PDF y de cada anuncio de boletín. Pero no le creo a ciegas: la salida tiene que pasar por un schema de Zod. El modelo no me devuelve texto libre, sino un objeto que cumple un contrato, y si no encaja lo descarto. Y en el system prompt hay una instrucción que importa muchísimo con datos legales: "usá null cuando un campo no está, no inventes". Prefiero mil veces un dato vacío a uno inventado; una fecha de cierre equivocada en una app de vivienda no es un detalle menor.

Son dos extracciones distintas, una para las convocatorias en PDF y otra para los anuncios de boletín, y las cacheo por hash (el SHA-256 del texto) para no procesar dos veces el mismo documento. Tampoco quería que dependiera de una API key: si no hay una, la extracción cae al CLI local de claude -p, y si tampoco está, devuelvo null y sigo con lo que saqué del HTML. La IA suma un montón, pero el sistema no se cae sin ella.

El otro frente sucio son los boletines. Buscar por palabra clave te trae como diez veces más ruido que cosas útiles: licitaciones de suelo, sanciones, adjudicaciones que ya se resolvieron. Ahí Claude hace de filtro: cada anuncio pasa por una extracción que decide si siquiera es una oferta de vivienda, y ninguno se publica sin pasar por esa validación.

Codificar las reglas: lo aburrido y lo crítico

Si la extracción con IA es lo vistoso, el motor de elegibilidad es lo aburrido. Y, lejos, es lo que más me costó dejar bien.

Calificar para vivienda protegida en Cataluña no es pasar un solo umbral. Depende del IRSC, del régimen de la vivienda, de la zona del municipio y de cuántos sean en el hogar. El IRSC 2026 es de 801,85 € por mes, 9.622,18 € al año; sobre ese número se aplican los multiplicadores de cada régimen, y el ingreso se pondera con dos coeficientes:

// ingreso ponderado = ingreso bruto anual × C[zona] × F[miembros]
const weightedIncome = grossAnnual * zoneCoefficient[zone] * memberFactor[members];

El coeficiente de zona y el factor por composición del hogar salen tal cual del decreto, así que tengo los 947 municipios de Cataluña mapeados por zona y por si son área de demanda fuerte (ADFA). Hay reglas que parecen un detalle hasta que ves a quién afectan: si alguien en el hogar tiene una discapacidad, el cálculo cuenta como si hubiera una persona más, y eso sube el tope de ingresos. En el código son un par de líneas; para esa familia puede ser la diferencia entre entrar o quedar afuera.

Esta parte no sirve de nada si está mal, así que la blindé con tests: los 8 topes de ingresos publicados para la Zona A me dan al céntimo, y si una refactorización mueve un solo euro, el build se queja. Y me autoimpuse una regla para todo el motor: ante la duda, no dejar a nadie afuera. Si un municipio no está en el mapa, cae en una clasificación conservadora. Prefiero mostrarte una oferta de más antes que esconderte una para la que sí calificabas.

Sin nada que venderte

Buscar vivienda protegida te deja con dos tipos de fuentes, y ninguna juega para vos. Por un lado, los portales de inmobiliarias y agencias: su negocio es venderte algo, así que tienen todos los incentivos para decirte lo que querés escuchar. Por el otro, las webs oficiales: no te venden nada, pero están tan enterradas en burocracia y tan mal hechas que entender tu situación se vuelve un trabajo en sí mismo.

Piso Propio no es ninguna de las dos. No tengo nada que venderte, y por eso me puedo dar el lujo de ser honesto. Igual la tentación existe: un "¡Calificás!" gigante en verde y listo. Pero calificar no es ganar.

La vivienda protegida casi siempre se sortea, y entre ser elegible y que te toque puede haber un abismo. El ejemplo que uso en la app es real: la promoción de Illa Glòries, en Barcelona, tuvo 11.243 solicitudes admitidas para 238 viviendas. O sea, más o menos una adjudicación cada 47 solicitudes.

Por eso Piso Propio te muestra las chances reales en vez de venderte humo. La promesa es esa y no más que esa: te ayuda a no perder un plazo y a entender dónde estás parado. No te promete una casa. Bajarle el tono triunfalista a toda la interfaz fue, para mí, la decisión de producto más importante.

El stack y el deploy

Por abajo el stack es bastante aburrido, y es a propósito. Lo interesante son los datos, no la infra.

  • Frontend y API: TanStack Start (React 19) con tRPC v11.
  • Base de datos: PostgreSQL 16 con Drizzle, unas 20 tablas.
  • Colas: BullMQ sobre Redis 7.
  • Almacenamiento: MinIO, donde guardo un snapshot de cada fetch para siempre.
  • Auth: Better Auth. Notificaciones: Resend y Web Push. Idiomas: castellano, catalán e inglés.

Lo de los snapshots lo pienso aparte. Cada vez que el sistema baja un HTML o un PDF, lo guarda con su hash de contenido. Eso me da dos cosas que terminaron siendo clave: puedo volver a extraer todo cuando sale un modelo mejor, sin volver a molestar a las fuentes, y puedo rastrear de dónde salió cada dato meses después. En algo que la gente usa para tomar una decisión real, saber de dónde viene cada cosa no es negociable.

El deploy es a propósito simple: un VPS de Hetzner con Docker Compose, manejado con Coolify (caddy, web, worker, scheduler, postgres, redis y minio). Y hay una separación que me deja dormir tranquilo: producción es una réplica de solo lectura, con el scraping apagado. Toda la recolección y el enriquecimiento con IA corren en mi máquina, y a producción solo le sincronizo el resultado ya verificado.

Construido con Claude Code

Casi todo lo armé con Claude Code, apoyándome en skills concretas: deep-research para entender la maraña de reglas de la vivienda protegida antes de escribir una línea, impeccable para que la interfaz no terminara pareciendo un formulario de trámite, y el set de skills de SEO y GEO de aaron-he-zhu para el posicionamiento y la optimización para motores generativos. La IA no está solo adentro del producto: también fue parte de cómo lo construí.

Lo que me llevo

Lo más raro de haber construido esto es que la IA, la parte que suena impresionante, no fue ni cerca lo más difícil. Lo difícil fue el resto: codificar reglas legales y verificar que dieran exactas, guardar la procedencia de cada dato, filtrar el ruido de los boletines sin dejar pasar avisos que no son ofertas, y bancarme la decisión de mostrar probabilidades honestas en vez de inflar expectativas.

Claude me resolvió algo que hace un par de años era casi imposible: leer PDFs oficiales feísimos y devolver datos en los que puedo confiar. Pero esa confianza no sale del modelo solo, sale de rodearlo de un schema, un caché, un fallback y un test que rompe el build. La IA rinde mucho más cuando la tratás como una pieza más del sistema, con sus límites, y no como una caja mágica que siempre tiene razón.