2026Solo developer — data pipeline, API and web app
Bailoteca — Dance school directory
A directory of dance academies in Santiago de Chile, fed by a human-in-the-loop data pipeline.
- Python
- FastAPI
- Angular
- PostgreSQL
- Supabase
- Pydantic
- GitHub Actions
- Data Pipeline
Problem
Dance academies in Santiago are scattered across Google Maps, Instagram and word of mouth. Anyone searching 'salsa classes in Ñuñoa' gets a mix of valid schools, aggregators and outdated listings — there is no single, reliable place to look.
I wanted a directory where every published entry was verified, not guessed by a scraper: real address, real website, real dance styles. That rules out fully automatic publishing by design.
Approach
Human-in-the-loop publishing
The pipeline ends at 'propose', never at 'publish'. A small CLI (`bailoteca review` / `bailoteca approve <id>`) lets a human check each proposal and publish it. I rejected fully automated publishing: scraped data is noisy, and one wrong address erodes trust faster than a missing listing.
Published rows are exposed to the public only through a read-only Postgres role with row-level security, so the API cannot write by accident.
Public sources only — no Instagram scraping
The enricher reads the public parts of the web: the academy's site, JSON-LD structured data, Google Maps and OpenStreetMap. For social presence it reads only the public Open Graph preview — never logs in, never walks through posts.
I rejected scraping Instagram feeds: it breaks ToS, it is brittle, and the signals it would add (latest post date) do not justify the maintenance cost.
Rules first, LLM as fallback
Style classification is deterministic rules on keywords and structured data; an LLM is used only for the cases the rules cannot resolve. This keeps the pipeline cheap and auditable — every decision can be explained from the input.
I rejected an LLM call per candidate: it multiplies cost per run and makes regressions impossible to pin down.
One schema, three surfaces
A shared package (`bailoteca-shared`: connection, Pydantic DTOs, reads) is the single source of truth consumed by the pipeline, the read-only FastAPI and the Angular SPA. If the schema changes, all three fail type-checking together instead of drifting apart.
FastAPI serves the same DTOs the pipeline produces; the Angular app consumes the same API as any external client.
Results
- Live in production at bailoteca.vercel.app with the first batch of real academies (34 listed at the time of writing) — the listing keeps growing because the pipeline proposes and I approve.
- Runs at $0/month: Supabase free tier, GitHub Actions and free data sources only.
- Read-only API in production at bailoteca-api.vercel.app/docs (OpenAPI via FastAPI).