SaltCast
Documentation ← Back to App

How SaltCast Works

SaltCast analyzes real-time tidal, weather, lunar, and water temperature data to predict the best saltwater fishing windows — updated every hour for 10 coastal locations.

The Overall Score

Every hour at every location gets a score from 0–100. This score combines four factors, each weighted by how much it actually influences saltwater fish behavior:

🌊 Tides30%
💨 Weather30%
🌙 Moon / Solunar25%
🌡️ Water Temp15%

The raw weighted score is then run through a power curve that pulls average conditions down significantly. This means a 90+ score is genuinely exceptional — not just a good tide on a calm day.

What Each Factor Measures

Tides (30%)

Rising tides score highest — baitfish push into the shallows, predators follow. Falling and slack-low tides score lowest. Data comes from NOAA tide prediction stations closest to each location.

Tide ConditionScore
Rising, upper half90
Rising, lower half65
Slack high40
Falling, upper half25
Falling, lower half15
Slack low5

Weather (30%)

Light winds (under 10 kts) and stable high pressure produce the best scores. High winds chop up the surface, disrupt baitfish, and make fish lock tight to structure. Heavy cloud cover slightly reduces scores as it limits light penetration.

Moon / Solunar (25%)

Solunar theory holds that fish feed most aggressively during major periods (around moonrise and moonset, roughly every 12.4 hours) and less intensely during minor periods (every 6 hours). Between periods, fish tend to be inactive.

Solunar PeriodScore
Major period90
Minor period50
Between periods10

Water Temperature (15%)

The sweet spot for most Gulf Coast and Atlantic inshore species is 65–78°F. Below 55°F or above 85°F, fish become lethargic and feeding activity drops significantly.

The Fish Scale

Each hour in the slideout panel shows 0–4 fish icons representing fishing quality for that specific hour:

🐟🐟🐟🐟
Prime
Major solunar + high overall score. Get on the water.
72+ & major
🐟🐟🐟
Good
Strong conditions — active bite expected.
72+
🐟🐟
Decent
Worth fishing — fish are moving.
60–71
🐟
Slow
Below average — be patient.
52–59
— slow
Dead
Poor conditions — consider waiting.
<52

The Heatmap Strip

Each day card in the 7-day forecast shows a 24-segment color strip — one segment per hour. The color tells you at a glance when the bite turns on:

Bright green
Score 70+ — prime fishing window. Plan to be on the water.
Yellow-green
Score 58–69 — decent conditions, worth going.
Amber
Score 45–57 — marginal, fish will be selective.
Dark navy
Score <30 — dead zone, save your energy.

The AI Fishing Forecast

Each day in the slideout panel includes a plain-English forecast written by Claude (Haiku) — Anthropic's AI. The forecast synthesizes the day's conditions into specific advice: which species will be active, what tactics to use, and what time to be on the water. It's generated fresh with every pipeline run.

Data Sources

NOAA Tides & Currents
Hourly tide predictions and water temperature from official NOAA tide gauge stations at each location.
Open-Meteo
Free, open-source weather API providing hourly wind speed, wind direction, surface pressure, and cloud cover.
Solunar Calculation
Major and minor feeding periods calculated from lunar position using the synodic month cycle (29.53 days).
Anthropic Claude
AI-generated plain-English fishing narrative for each day, based on the computed conditions and scores.

Score Alert Emails

SaltCast automatically emails bob@rensho.ai whenever any location scores 90 or above in the next 24 hours. The email includes the location, peak score, best time windows, tide direction, and wind speed — so you can plan a trip on short notice when conditions align perfectly.

Technical Architecture

SaltCast is a fully automated prediction engine built on a Hetzner VPS using n8n for orchestration, Supabase (self-hosted) for storage, and a static HTML frontend served by Caddy.

Stack Overview

n8n
Orchestration
Data pipeline + AI
Supabase
PostgreSQL
Single source of truth
Caddy
Static frontend
saltcast.rensho.ai
ComponentDetails
ServerHetzner VPS — root@5.161.91.134
n8nDocker container (custom image with ffmpeg). Access at n8n.rensho.ai. Do NOT restart with docker compose — use docker restart n8n
SupabaseSelf-hosted Docker stack at supabase.rensho.ai. Schema: saltcast
CaddyDocker container. Config: /opt/rensho/caddy/Caddyfile. Reload: docker exec caddy caddy reload --config /etc/caddy/Caddyfile
Frontend/opt/rensho/www/saltcast/ — single index.html
Important: When editing the Caddyfile, always use scp from your Mac to write directly to the original file inode. Using cat > or Python open('w') creates a new inode that the Docker bind mount won't follow.

n8n Workflows

SaltCast — Fishing Prediction Pipeline (HlkH6OgQRoD9BUgJ)

Currently inactive (manual trigger while testing). Set to hourly once stable. Takes 5–8 minutes per full run across all 10 locations. Max concurrency = 1 prevents overlapping runs.

NodePurpose
Every HourSchedule trigger — change interval here
Get Active LocationsFetches all locations where active=true from Supabase
Loop Over LocationsIterates one location at a time
Fetch WeatherOpen-Meteo API — wind, pressure, clouds (7-day hourly)
Fetch TidesNOAA Tides & Currents — hourly predictions (7-day)
Fetch Water TempNOAA water temperature. continueOnFail=true, retryOnFail=true (NOAA timeouts are common)
Calculate ScoresCore scoring algorithm — weather, tide, solunar, water temp → power curve → overall score
Batch by Day / Upsert PredictionsUpserts 168 hourly predictions per location to Supabase
Build Daily SummariesAggregates 24 hours into daily summary stats
Build Narrative PromptsConstructs plain-text prompt for each day
Generate NarrativesCalls Claude Haiku for each day's AI forecast (max 300 tokens)
Merge NarrativesMerges narrative text back onto summary objects
Upsert SummariesWrites to daily_summaries table. Empty response = success (204 No Content)

SaltCast — Score Alert (90+) (NFEHqoCMw5GdLaTI)

Active, runs hourly. Queries predictions ≥90 in next 24 hours, sends styled HTML email from saltcast@rensho.ai via Resend SMTP (credential ID: dRfO8HPussxphvHM).

Supabase Schema

All tables live in the saltcast schema. PostgREST is configured to expose this schema via PGRST_DB_SCHEMAS.

locations

id uuid PRIMARY KEY
name text (e.g. "St. George Island, FL")
lat numeric
lng numeric
noaa_station_id text (NOAA tide gauge ID)
timezone text (e.g. "America/Chicago")
active boolean (false = excluded from pipeline)

predictions

id uuid PRIMARY KEY
location_id uuid REFERENCES locations
forecast_time timestamptz
overall_score integer (0–100, power-curved)
weather_score integer
tide_score integer
moon_score integer
water_temp_score integer
wind_speed numeric (knots)
tide_height_ft numeric
tide_direction text (rising/falling/slack-high/slack-low)
water_temp_f numeric
moon_phase numeric (0–1)
is_solunar_major boolean
is_solunar_minor boolean
UNIQUE (location_id, forecast_time)

daily_summaries

id uuid PRIMARY KEY
location_id uuid REFERENCES locations
forecast_date date
avg_score integer
max_score integer
best_window_start integer (hour 0–23)
best_window_end integer (hour 0–23)
moon_phase numeric
moon_name text
tide_highs_count integer
ai_narrative text (Claude-generated)
UNIQUE (location_id, forecast_date)

Scoring Algorithm

The scoring pipeline runs entirely in the Calculate Scores n8n Code node. Key design decisions:

Upsert, not insert
Every pipeline run overwrites existing rows using the unique constraint. Database stays at a fixed size — 10 locations × 168 hours = 1,680 prediction rows.
Power curve
Raw scores are transformed via Math.pow(score/100, 1.35) × 100. This pulls average conditions (score 60–70) down to 47–57, making high scores genuinely meaningful.
Timezone-aware dates
forecast_date generation uses the location's local timezone, not UTC. Prevents date shift for locations where the pipeline runs near midnight UTC.
NOAA fallback
Water temp node has continueOnFail=true and retryOnFail=true. If NOAA times out, scoring continues with 72°F fallback. Prevents one flaky API from killing the whole run.

Frontend Architecture

The frontend is a single static HTML file — no build step, no framework, no dependencies beyond Supabase JS and Google Fonts. The entire app is one file: /opt/rensho/www/saltcast/index.html.

Data flow

On load, the frontend resolves the selected location's UUID from Supabase, then fetches 7 days of predictions. If Supabase is unavailable, it falls back to locally-generated mock data so the UI is never blank.

// Boot sequence
populateLocationDropdown() // load locations from Supabase
resolveDefaultLocation() // get UUID for St. George Island
fetchData() // load 7-day predictions
render() // draw heatmaps, gauge, conditions

// On day card click
openPanel() // show slideout
renderHourly() // draw 24-hour fish timeline
fetchNarrative() // read ai_narrative from daily_summaries

Deploy pattern

# From Mac — bundle and deploy
scp ~/Downloads/deploy.tar.gz root@5.161.91.134:/tmp/
ssh root@5.161.91.134 'cd /opt/rensho/www && tar xzf /tmp/deploy.tar.gz && rm /tmp/deploy.tar.gz'

# tar structure
saltcast/
saltcast/index.html
saltcast/images/saltcast-logo.svg
saltcast/images/saltcast-favicon.svg
saltcast/images/rensho-logo.png

Credentials

ServiceCredentialn8n ID
AnthropicAnthropic accountLguuwcjilf20kdRK
Resend SMTPSMTP accountdRfO8HPussxphvHM

Adding a Location

Insert a row into saltcast.locations with the location's coordinates, NOAA station ID, and timezone. The pipeline will automatically pick it up on the next run.

INSERT INTO saltcast.locations (name, lat, lng, noaa_station_id, timezone, active)
VALUES ('Your Location, ST', 00.0000, -00.0000, '0000000', 'America/Chicago', true);

Find NOAA station IDs at tidesandcurrents.noaa.gov — search for the nearest tide gauge to your location.