DEV Community

ANIRUDDHA  ADAK
ANIRUDDHA ADAK Subscriber

Posted on

I built a surf forecast that shows its own arithmetic

Live: https://swellread.vercel.app
Code: https://github.com/aniruddhaadak80/swellread

Here's the thing nobody tells you about surf forecasts: they are already telling you the truth, and it is still useless to you.

A forecast gives you a significant wave height, a period, a direction, a tide table and a wind arrow. That is enough information to know exactly whether a two-hour drive is worth it. But nobody has ever shown me what those numbers mean — so "1.4 m at 14 s from 155°" stayed a string of characters, and the decision stayed a feeling. The feeling was wrong about half the time. That is an expensive way to learn.

So I built the thing I wanted: Swellread, which turns today's real swell, wind and verified tide predictions into one verdict per break, and then shows every number that produced it.

The tide drag re-cutting the reef cross-section

The one gesture that explains the whole thing

Drag the tide. That is the entire idea.

Peel speed is shallow-water celerity, c = √(g·depth). The same 1.66 m swell over a 0.8 m take-off peels at about 12 km/h and is unridable. Over 2.4 m it peels at 21 km/h and is the best thing on the coast. Nothing about the swell changed.

So the app draws the break's actual seabed profile, lets you move the water level, and re-solves the peel. It is not an animation: the drag POSTs a what-if to the server, which runs the same scoreHour function the page, the REST API and the agent tools all call, and returns a new SHA-384 seal. The number next to it changes because the arithmetic changed.

What it actually does

  • 24 scored hours, not one number for the day. Each hour is a link, so the exact slice you are looking at is the URL you can send someone.
  • Six weighted factors, each showing its arithmetic. Swell power (P = ⅟₁₆ρgHs²Tp), peel speed, wind quality, tide window, direction match, period cleanliness. They sum to exactly 1, then get renormalised over whatever has real data behind it.
  • A factor with no data is dropped and labelled, never quietly scored at zero. Six of the fourteen breaks have no NOAA tide station in range — including Kovalam and Arugam Bay on this coast — so the tide factor is removed and the app says "No NOAA CO-OPS station is in range for this break, so no tide value is available." It does not interpolate a tide.
  • Hard gates that override the score. Below 0.35 m of swell the score is zero, because there is nothing to break on. Below a reef's minimum safe depth it is clamped to 0.08, because that is a hazard rather than a low score.
  • Real data, honestly labelled. NOAA CO-OPS tide predictions and Open-Meteo marine + forecast, all keyless. Every source row carries its licence, its fetch time, and whether it is live or fallback.

Why open mattered

The tide problem has no open answer, so I had to stop faking it. NOAA's public API only publishes predictions for the US and a scattering of Pacific islands — essentially nothing around the Bay of Bengal. The honest engineering answer was not to approximate it. It was to remove the factor and renormalise the weights, and then have the interface admit it on every single page. A closed product with a subscription would have shown a confident number anyway.

The AI runs on the rider's device, and that is not a compromise — it is the correct tool. You write one line about how the water felt. A sentence-embedding model (all-MiniLM-L6-v2, Apache-2.0) reads it, labels the kind of session you are describing, and finds your own earlier sessions that felt the same. The int8 weights are committed to the repository and loaded with allowRemoteModels = false, so no model host is contacted and there is no API key anywhere in the app.

Where open beat closed, concretely:

  • The note never leaves the browser. A hosted embedding API would have shipped a private, sensitive sentence about someone's water to a third party. Mine cannot, by construction.
  • It costs nothing per use. No per-request billing, no rate limit, no quota to design around.
  • It runs with the network off once the runtime is cached — the right behaviour for a beach.
  • It is swappable. Change one id and drop in different weights. Nothing assumes MiniLM.

A playable wave built from the same numbers

The WebGL wave lab

The surface is a Gerstner sum built from the same height, period and heading as the rest of the app. It breaks where the water is shallow enough (H/d = 0.78), and the peel travels at the engine's own speed. You have to stay in the pocket between the section and the foam: get ahead of it and it closes out, fall behind and you are done. A/D steers, W pumps, Space kicks out.

It is a Gerstner surface, not CFD, and the riding is a score rather than a physics engine with a real surfer in it. I am happy to say that out loud — take the water model seriously and the gameplay lightly.

An agent that can read the water and write a plan

Eight typed tools over JSON-RPC 2.0, four of them mutating, all through the same service layer as the web UI:

curl -s https://swellread.vercel.app/api/mcp \
  -H "content-type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
Enter fullscreen mode Exit fullscreen mode
{
  "mcpServers": {
    "swellread": {
      "type": "http",
      "url": "https://swellread.vercel.app/api/mcp"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

An agent can rank a day, freeze a plan, record your call, log a ride and prove the chain — and it cannot touch another rider's session, because sessions are scoped to an anonymous HTTP-only cookie and there is no way to name one.

You can try all of it in the in-page console with the exact request and response on screen.

Plans you can hand over, and prove later

The exportable brief

Every session freezes its conditions and verdict, and appends a link to a per-entity chain:

seal_n = SHA-384( UTF-8(prevSeal) || canonicalJson(event_n) )
Enter fullscreen mode Exit fullscreen mode

Deleting a session is a soft delete that keeps a tombstone, so the history stays replayable forever — and the replay tool names the first broken link if there is one. This is not decoration: I built it because "I told you it would be this good" is worthless six weeks later when you cannot prove it.

The bugs that mattered

Three of these were silent, which is the interesting part.

  1. Both Open-Meteo endpoints nest their arrays under a hourly object. I was reading them at the top level. The request succeeded, the arrays were missing, and the app happily served its offline sample while every page claimed to be live. A live verifier caught it. It now has tests against captured real response shapes.

  2. The Neon serverless driver's tagged template rewrites your statement. I had faked a TemplateStringsArray around my $1-style queries. Every parameterised statement in the repository came back as column excluded.blurb$1 does not exist. Unit tests passed the whole time, because they ran on embedded PGlite. The fix was the driver's actual positional-parameter entry point, sql.query(text, params) — and the lesson was to test the production driver, so there is now a hosted-store suite that runs whenever DATABASE_URL is set.

  3. The wave lab multiplied peel speed by 1000, so the section raced 218 m per frame and every ride ended instantly. Found by playing it, not by reading it.

Honest limitations

  • The rate limiter is in-memory and therefore per serverless instance. It raises the cost of a hammering loop; it is not a hard limit, and the README says so rather than implying otherwise.
  • Anonymous ownership is cookie-scoped. Clear your cookies and you lose access to your sessions. That is the trade for having no accounts and no secrets.
  • Break coordinates describe real coastlines. Peel orientation, reef slope and take-off depth are my own editorial estimates, and they are labelled as estimates everywhere they appear.
  • It describes the surface of the ocean. It cannot see the reef, the current or your ability. It is a planning aid, never a safety guarantee.

Try it in thirty seconds

git clone https://github.com/aniruddhaadak80/swellread
cd swellread
npm ci
npm run dev
Enter fullscreen mode Exit fullscreen mode

No environment variables, no keys, no signup. It runs on an embedded PGlite database, seeds its break catalogue, and talks to the real NOAA and Open-Meteo APIs.

Two checked-in scripts prove the whole thing: npm run verify (36 HTTP assertions against a live deployment) and scripts/browser-smoke.mjs (15 assertions through real visible controls, including the tide drag, the ride loop, mobile and reduced motion). Both currently pass 36/36 and 15/15 against production.

The handover

Thanks for reading. If you know someone who drives a long way to a break, send them the link — the tide drag takes about ten seconds and it is the thing that makes the rest click.

Top comments (0)