Sapcast


When should I tap my maple tree? A forecast app that matters for about three weeks a year.

Started 2026 · Live TypeScript · Cloudflare Workers · KV

Maple sap runs when the nights freeze and the days thaw. Miss that window and you get nothing, and the window moves every year inside a season that only runs about three weeks. The traditional answer is to watch the weather and go on feel. I wanted to know whether the forecast could just tell me.

Sapcast takes your location, pulls a 7-day forecast from four weather models, scores each day’s freeze-thaw cycle against published sap-flow research, and recommends when to tap. One screen, no accounts, no navigation.

The whole picture

Sapcast asks for your location, then answers the only question that matters: should I be tapping?

Sapcast forecast overview, showing a five-day excellent window starting today

Top to bottom: where you are and current conditions, a map to confirm it got the right spot, the headline recommendation, how your latitude’s season is tracking, and the seven-day breakdown.

Working out where you are

Sapcast asks the browser for your location on load. Nothing is typed, and no account exists.

Loading state with an animated maple leaf and sap drops, plus the help hint that appears after 13 seconds

If the permission prompt goes unanswered for 13 seconds, a hint appears with OS-specific instructions, since iOS, Android and desktop each need different advice. Once located, the header shows the coordinates, current temperature and conditions.

Header showing coordinates, current conditions and the unit toggle

If location is blocked

Denying the prompt isn’t a dead end. You get the reason, the OS-specific help, a retry, and a postal code fallback.

Location denied, with an error message, troubleshooting hint, try again button and postal code fallback

Changing location on purpose

Useful when the sugarbush isn’t where you’re standing. Change location opens a postal code box, and codes starting with a letter are treated as Canadian FSAs, digits as US ZIPs.

Change location panel with a postal code entered

The lookup is geocoded server-side and cached for 30 days, and the map moves to match.

Forecast re-centred on postal code K0A, west of Ottawa

Choosing your units

Everything is scored in Celsius internally, so the toggle only changes what you read. It re-renders in place with no refetch.

Unit toggle switched to Fahrenheit

The same seven-day forecast in Fahrenheit

The recommendation, in five flavours

The recommendation comes from the longest consecutive run of good-or-better days, and reads differently depending on what the week looks like.

State When you see it Colour
Tap now A qualifying run starts today Green
Upcoming The run starts later in the week Amber
Brief window The best run is a single day, not worth much sap Neutral
Too cold Nothing rises above freezing, so the season hasn’t started Blue
Season over No freezing nights left in the forecast Red

Tap now. Five straight freeze-thaw days from -5 °C to 6 °C onward. The average score is high enough to be called excellent rather than merely good, and past three days it adds the “great stretch” note.

Tap now, with excellent conditions for the next 5 days

Upcoming. The week opens too warm to freeze, then a solid three-day run arrives on the Friday.

Great window coming Friday March 13, with 3 days of favourable conditions

Brief window. There is a perfect day in there, but one day alone doesn’t fill buckets, so Sapcast says so instead of overselling it.

Only a brief 1-day window coming Thursday March 12

Too cold. A deep freeze where highs never clear 2 °C. No thaw, no flow.

Too cold, with daytime temperatures not yet rising above freezing

Season over. Every night stays above freezing. Time to pull your taps.

Season may be over, with no freezing nights in the forecast

Season timing

The forecast only sees seven days, which isn’t enough to tell you that you’re running out of March. This card interpolates typical tap-by and season-end dates from your latitude, so a quiet forecast in late season reads as tap anyway rather than wait.

Season timing, showing tap by Friday March 20 and season end Friday April 3

At 45.35° that lands on 20 March and 3 April. Further south both dates move earlier, further north, later.

The 7-day forecast

Each day gets a rating and a colour bar from its overnight low and daytime high.

Seven-day forecast with excellent, good and poor days

  • Excellent. Low between -7 °C and -2 °C, and high between 4 °C and 10 °C
  • Good. Freezes overnight and thaws above 2 °C, with one of the two in the ideal band
  • Fair. Freeze-thaw happens, but both numbers sit outside the ideal range
  • Poor. No freeze-thaw at all, so either it never froze or it never thawed

The ratings make the shape of the week obvious at a glance. Here the first three days never freeze properly, then the run arrives.

Forecast where poor days give way to an excellent run

And a scrappy week, a mix of fair and poor around one good day.

Forecast with a marginal mix of fair and poor days

Where the numbers come from

The high and low for each day are averaged across four forecast models, which smooths out any single model’s bad call. Tap any day to see what each model actually said.

Expanded day showing Pirate Weather, ECMWF, GEM and GFS values

When the models disagree sharply, that’s a signal the day is genuinely uncertain, which is worth knowing before you commit to a boil.

Knowing when is only half of it

Sapcast isn’t only a timing tool. The bottom of the page is a standing reference for doing the work, and it’s visible without JavaScript, without location permission, and out of season.

How It Works explains the mechanism and what each rating means, so the numbers above aren’t a black box.

How It Works, covering the freeze-thaw cycle, what the ratings mean, and why consecutive days matter

Tapping Guides covers the full season: choosing and drilling a tree, collecting and boiling, when to pull your taps, and end-of-season cleanup, each with a source you can go read.

Tapping Guides, covering choosing and tapping a tree, collecting and boiling, when to pull taps, and cleanup

The thresholds aren’t invented, and the footer says where they come from: sap-flow research plus the university extension programs that study it.

Sources and further reading, listing research papers, extension programs and weather data credits

Telling me how it went

A collapsed feedback panel, because the model is still being tuned and a tapper’s “this was wrong” is worth more than another threshold tweak.

Feedback panel with a thumbs-up selected and a comment typed

On a phone

The layout is built for being read in a barn coat with cold hands: single column, large touch targets, and the recommendation above the fold.

Sapcast on a phone, showing the recommendation and season timing

Forecast rows reflow so the day, temperatures and rating stay readable at 390 px wide.

Seven-day forecast on a phone

How it’s built

The whole thing is a single Cloudflare Worker with no build step and no runtime dependencies. Wrangler handles the TypeScript, KV caches forecasts for three hours on a roughly 11 km grid, and postal code lookups are cached for 30 days because postal codes don’t move. The four models are fetched with Promise.allSettled, so one provider having a bad afternoon degrades the average instead of breaking the page.

The scoring is pure and separate from the Worker, which is what made the demo server possible.

Demoing an app that works three weeks a year

This was the interesting problem. For most of the year Sapcast is untestable by inspection: no freeze-thaw in the forecast means every day rates poor, the recommendation is stuck on one state, and there’s nothing to look at. Screenshots go stale, and mocking up the five recommendation states in a design tool would prove nothing about whether the code produces them.

So demo/demo-server.mjs proxies the real frontend from wrangler dev and answers /api/forecast with canned temperatures, run through the production scoring code. /api/geocode still passes through to the real Worker, so postal code lookup works for real. Scenarios switch at runtime without a restart:

1
2
3
4
5
6
7
8
npm run dev                  # real worker on :8787
node demo/demo-server.mjs    # demo proxy on :8788

curl localhost:8788/_scenario/tap-now      # 5-day run from today
curl localhost:8788/_scenario/upcoming     # 3-day run from Friday
curl localhost:8788/_scenario/marginal     # a single good day
curl localhost:8788/_scenario/too-cold     # nothing thaws
curl localhost:8788/_scenario/season-over  # nothing freezes

It imports scoring.ts and weather.ts directly rather than reimplementing them, using Node’s native type stripping, so there’s no build step in the demo harness either and no chance of the demo drifting from production. Every screenshot on this page came out of it.

Checking the model against a bucket

Scoring days against published research is one thing, and knowing whether the ratings match what came out of the tree is another. There’s a data/sap-collection.csv in the repo where I log collection dates, the temperatures for that day, the rating Sapcast gave it, and how much I actually collected. It’s one season of thin data so far and it doesn’t prove anything yet, but over a few more it should say whether the thresholds need moving.

The other half of the project was a developer experience experiment. I wrote most of it from my phone, in Claude Code sessions, to find out whether project instructions, tests and CI could carry enough of the load that a phone becomes a workable place to build something. On a desktop you route around a snag fast enough that you never register it as a snag, and on a phone there’s nowhere to hide. That’s its own post, and everything I fixed to make phone development survivable made desktop development better too.