Side project: there is no client, I built it to study or to experiment.
Case study
- My role
- All of it, alone: product, verdict rules, bot, tests and deployment on Fly.io. I wrote the code with Claude Code: I decide what to build and how, and every change goes through typecheck and tests before it reaches production.
- Duration
- July to August 2026
The problem
Wind forecast apps already give you every number: knots, gusts, direction, waves. They don't tell you whether to go. Someone arriving in Tarifa from elsewhere can't read them for that spot yet, and a good day for an expert may not be one for a beginner. What was missing was a single answer, with a reason you can trust. The constraints were real. Goable's free plan allows 50 calls a day and does not accept the rider's level. The launch covers a single spot. The bot has to run as a single process, because Telegram delivers updates to one process at a time and the data lives in a SQLite file.
What I did and why
- The verdict before the numbers: every message opens with the 0 to 100 score, a colour (green, yellow or red) and the best hours, the unbroken block of hours above 90% of the peak. Only hours between 8:00 and 20:00 count, so a 3 a.m. wind spike does not decide the day. One line says whether tomorrow is improving, dropping or steady.
- The why in one line: the three factors that weigh most in Goable's score, each with a plain rating, such as "wind poor · wind dir good · wave good". The Why? button lists every factor with its percentage.
- One threshold per level, one no-go for everyone: Goable's free plan does not accept the rider's level, so the bot applies it to the score. A "go" takes 75 points for a beginner, 65 for an intermediate and 55 for an expert. When Goable reports that going out is impossible or dangerous, for instance because there is no wind, the verdict is no-go at any level. The bot follows the API's explicit signal, so a day of strong wind does not turn into a false no-go.
- Calls per spot, not per user: every 12 hours the bot refreshes Tarifa with two calls to Goable, the hourly series for today and tomorrow and the detail of today's peak, and stores everything in SQLite. Commands, buttons and the brief only read from there. A daily counter, checked before every call, stops at 45. A bad value in the config stops the bot from starting instead of lifting the limit.
- One brief a day, no more: at 7:00 Tarifa time each subscriber gets the verdict once, even if the process restarts. People who have blocked the bot are taken off the list. If a refresh is missed, the bot never describes today with yesterday's data: it drops the why and, after 18 hours, says the data is not fresh.
- Measuring the idea: the bot logs in SQLite the actions that show whether it is useful, such as opening Now, tapping Why? and sharing, storing nothing personal beyond the Telegram id. The shareable verdict carries a link back to the bot with a reference, so new users coming from group chats can be counted. On "go" days a Book button, not live yet, measures who would book.
- One process, deploys only on green tests: on Fly.io the bot is a worker with no open ports, on a single machine bound to a single volume, because Telegram long polling and SQLite need one process. On every push to main, GitHub Actions runs the typecheck and the 22 tests and only then deploys. On shutdown, cron jobs, bot and database close in order within 5 seconds. Dependabot proposes updates every week.
Results
- 1 dayfrom the first commit to the first automated deploy on Fly.io
- 4API calls a day to Goable, however many people subscribe
- 0API calls for commands, buttons and the brief, all served from the cache
- 3rider levels, each with its own threshold for a "go"
- 07:00the morning brief, sent once even if the process restarts
- 22automated tests, run before every deploy
Stack highlights
- Node.js 22
- Typescript
- grammY
- Goable SDK
- SQLite (better-sqlite3)
- node-cron
- node:test
- Fly.io
- Docker
- Github Actions
People heading to Tarifa open the chat with @GoSignalApp_bot, pick their level and every morning at 7:00 get the day's verdict: score, best hours, a one-line why and whether it's worth going out at their level. During the day they can check it again, open the detail of every factor, compare today with tomorrow and forward the verdict to the crew they ride with. On days without wind the bot says no-go to everyone, whatever their level.
What I bring to an agency or a small business from this project
- A service in production on the day of the first commit, built with Claude Code, with typecheck and tests that block the deploy when something breaks.
- Third-party API costs kept in check by design: calls depend on the spot, not on the users, and a daily cap stops them.
- Technical data turned into a decision a person understands: a verdict, a one-line why, the detail one tap away.