Feature spec
IntermediateCategory
notifications-alerts
Build with AI
4–6 hours with V0 or Lovable
Custom build
1–2 weeks custom dev
Running cost
$0–20/mo up to 100 users · $100–200/mo at 10K users
Works on
Everything it takes to ship Stock Alerts — parts, prompts, and real costs.
Stock alerts need four pieces: an alert creation form with ticker validation, a price polling engine (Supabase Edge Function or Vercel Cron calling Finnhub or Alpha Vantage every 1–5 minutes), an alert evaluation worker that deduplicates with a cooldown column, and a notification sender (email via Resend plus browser push via web-push). With V0 or Lovable you can ship a working alert system in 4–6 hours for $0–20/month at small scale — Finnhub's free tier covers 60 API calls/minute for polling dozens of unique tickers.
What Stock Alerts Actually Are
Stock alerts notify users the moment a price condition is met — 'tell me when AAPL drops below $180' or 'alert me when NVDA gains 5% in a day.' The mechanism is a polling loop: fetch the latest quote from a market data API every few minutes, compare against every active user alert, and fire a notification for any condition that has been crossed. The product decisions are: how fresh the data needs to be (polling delay vs real-time WebSocket streaming), which alert conditions to support (simple price thresholds vs technical indicators), and how to prevent the same condition from spamming the user every minute once triggered.
What users consider table stakes in 2026
- Create an alert by searching for a ticker symbol — not by typing a raw string — with autocomplete or validation to catch invalid symbols
- Alert conditions cover at minimum: price above, price below, and percentage change up or down from current price
- Notification fires within 5 minutes of the condition being met (polling-based) with the actual price at trigger time
- One-click alert management from a dashboard: create, pause, and delete without navigating multiple screens
- Alert history shows when each condition was last triggered and the price at that moment so users can audit their alerts
- Deduplication prevents the same alert from firing every minute while price stays on the trigger side of the threshold
Anatomy of the Feature
Seven components across four layers. V0 generates the UI and API routes well. The polling engine and the deduplication logic are where first builds break.
Alert Creation Form
UITicker search input using Algolia Autocomplete or a custom combobox querying a tickers list, a condition selector (price above / price below / change % up / change % down), and a threshold number input. Built with React Hook Form and Zod validation; saves to the stock_alerts table on submit.
Note: Validate the ticker symbol against the data API before saving — storing an invalid ticker means the polling engine will silently fail for that alert forever.
Ticker Lookup Service
ServiceFinnhub /symbol/search or Alpha Vantage SYMBOL_SEARCH endpoint validates ticker symbols in real time as the user types. Debounced on keypress (300ms). API key stored in Vercel environment variables or Supabase Secrets — never exposed to the frontend.
Note: Cache validated ticker symbols in a tickers table so repeated searches don't consume rate limit quota on every keystroke.
Price Polling Engine
BackendA Supabase Edge Function or Vercel Cron Job running every 1–5 minutes. Queries the stock_alerts table for all distinct active tickers, then calls the Finnhub /quote endpoint for each. Writes latest price, change_pct, and updated_at to the stock_prices cache table. Batches ticker requests to respect the 60 calls/minute free tier limit.
Note: Group all users watching the same ticker into a single API call — if 50 users have AAPL alerts, that is one Finnhub call, not 50.
Alert Evaluation Worker
BackendSQL function or Edge Function that joins stock_alerts to stock_prices and evaluates each active alert's condition against the current price. Only fires for alerts where last_fired_at IS NULL OR last_fired_at < NOW() - INTERVAL '1 hour'. Updates last_fired_at atomically in the same transaction as inserting into alert_history to prevent race conditions.
Note: Wrap the evaluation and last_fired_at update in a Postgres transaction — if two cron executions overlap without this, the same alert fires twice.
Browser Push Notification
ServiceWeb Push API delivery via the web-push npm package with VAPID keys stored in environment variables. A Supabase Edge Function or Vercel API route sends the push payload (ticker, current price, threshold, deep link) to the user's stored subscription endpoint in the push_subscriptions table.
Note: Handle 410 Gone responses from the push service by deleting the stale subscription row — re-register on every page load with an upsert.
Email Notification
ServiceResend transactional email sent from a Supabase Edge Function. HTML template includes the ticker symbol, current price at trigger, alert threshold, direction (above/below), and a direct link to the alerts dashboard. RESEND_API_KEY stored in Supabase Secrets.
Note: Send email and push in the same Edge Function invocation to reduce cold start latency — both are fast outbound HTTP calls.
Alerts Dashboard
UIA table of active alerts showing ticker, condition, threshold, live current price (polled via SWR with 60-second refresh), last triggered time, status badge (active/triggered/paused), and toggle to enable/disable. Delete button removes the alert and its history.
Note: The live price column should refresh on a 60-second SWR cycle — Supabase Realtime is overkill here and adds connection overhead for a low-frequency update.
The data model
Four tables cover alerts, price cache, notification subscriptions, and history. Run this in the Supabase SQL editor. Note that stock_prices is only readable by service_role since it contains raw market data your app controls.
1create table public.stock_alerts (2 id uuid primary key default gen_random_uuid(),3 user_id uuid references auth.users(id) on delete cascade not null,4 ticker text not null,5 condition text not null check (condition in ('above','below','pct_up','pct_down')),6 threshold numeric not null,7 last_fired_at timestamptz,8 is_active bool not null default true,9 created_at timestamptz not null default now()10);1112create table public.stock_prices (13 ticker text primary key,14 price numeric not null,15 change_pct numeric,16 updated_at timestamptz not null default now()17);1819create table public.push_subscriptions (20 id uuid primary key default gen_random_uuid(),21 user_id uuid references auth.users(id) on delete cascade not null,22 endpoint text not null unique,23 p256dh text not null,24 auth text not null,25 created_at timestamptz not null default now()26);2728create table public.alert_history (29 id uuid primary key default gen_random_uuid(),30 alert_id uuid references public.stock_alerts(id) on delete cascade not null,31 fired_at timestamptz not null default now(),32 price_at_fire numeric not null33);3435alter table public.stock_alerts enable row level security;36alter table public.stock_prices enable row level security;37alter table public.push_subscriptions enable row level security;38alter table public.alert_history enable row level security;3940create policy "Users manage own alerts"41 on public.stock_alerts for all42 using (auth.uid() = user_id)43 with check (auth.uid() = user_id);4445create policy "Service role manages stock prices"46 on public.stock_prices for all47 using (auth.role() = 'service_role');4849create policy "Users manage own push subscriptions"50 on public.push_subscriptions for all51 using (auth.uid() = user_id)52 with check (auth.uid() = user_id);5354create policy "Users view own alert history"55 on public.alert_history for select56 using (57 exists (58 select 1 from public.stock_alerts sa59 where sa.id = alert_id and sa.user_id = auth.uid()60 )61 );6263create policy "Service role inserts alert history"64 on public.alert_history for insert65 with check (auth.role() = 'service_role');6667create index stock_alerts_ticker_active_idx68 on public.stock_alerts (ticker)69 where is_active = true;7071create index stock_alerts_user_idx72 on public.stock_alerts (user_id, created_at desc);Heads up: The partial index on active tickers lets the polling engine quickly find all distinct tickers to fetch in a single query scan. The stock_prices table acts as a shared cache — all users watching the same ticker share one row, so polling cost scales with unique tickers, not with user count.
Build it — pick your path
Each tool fits this feature differently. Switch tabs to compare fit, time, the exact steps, and a copy-paste prompt.
Custom development is right when you need real-time WebSocket price streaming, complex technical indicator conditions, or multi-asset coverage (options, futures, crypto derivatives) — use cases that polling-based AI-generated builds cannot serve.
Step by step
- 1Polygon.io WebSocket Stocks Starter subscription ($79/mo) for sub-second price streaming; subscribe per ticker as users create alerts rather than polling all tickers on a schedule
- 2Redis (Upstash) as a shared price cache so the alert evaluation worker reads from memory rather than hitting the Polygon.io API per evaluation cycle
- 3Complex condition engine: RSI crossover, moving average breach, MACD signal — requires a time-series price buffer (pgvector or TimescaleDB) and indicator calculation logic
- 4Multi-channel delivery: Twilio SMS for critical alerts, Slack webhook for trader dashboards, in addition to email and browser push
Where this path bites
- Polygon.io WebSocket Stocks Starter adds $79/mo base cost before any per-user infrastructure
- Technical indicator calculations require a historical price buffer — additional storage and computation cost at scale
Third-party services you'll need
Stock alerts need a market data API for price feeds and a notification delivery layer. All three paths below have free tiers sufficient for an early-stage product.
| Service | What it does | Free tier | Paid from |
|---|---|---|---|
| Finnhub | Stock price API — GET /quote for current price, /symbol/search for ticker validation; the primary data source for the polling engine | 60 API calls/minute, 100K calls/day | Premium from $50/mo for higher rate limits and real-time data (approx) |
| Alpha Vantage | Alternative stock data API with adjusted close prices and intraday OHLCV data | 25 API calls/day | Premium from $50/mo for unlimited calls (approx) |
| Polygon.io | Real-time WebSocket streaming for sub-second price updates; replaces polling for high-frequency alert requirements | 15-minute delayed data | Stocks Starter $79/mo for real-time (approx) |
| Resend | Transactional email delivery for alert notifications; HTML template with price data and manage-alerts link | 100 emails/day, 3K/mo | Pro $20/mo for 50K/mo |
| web-push | Open-source npm package for browser push notification delivery using VAPID keys; no per-message cost | Open-source, zero cost | Free |
Swipe the table sideways to see pricing.
What it costs to run
Drag through the tiers to see how your monthly bill scales with users — no surprises later.
Estimated monthly running cost
Finnhub free tier covers polling for 100 users easily if they watch fewer than 60 unique tickers total. Resend free tier covers email volume. Supabase free tier handles DB storage.
Estimates use verified 2026 pricing and assume typical usage per user. Your real bill depends on activity, storage, and third-party plans.
What breaks when AI tools build this
The failures people actually hit on their first build — the symptom, why it happens, and the exact fix.
Alert fires repeatedly every minute instead of once per condition trigger
Symptom: The alert evaluation worker runs on a cron schedule. If it checks the condition and fires a notification but doesn't atomically update last_fired_at in the same database transaction, the next cron run finds the same condition still met and fires again. If two cron executions overlap — which happens when the cron period is shorter than the evaluation runtime — both instances update last_fired_at after both have already decided to send, resulting in duplicate notifications for the same trigger event.
Fix: Wrap the alert evaluation in a Postgres transaction: SELECT ... FOR UPDATE SKIP LOCKED on the alert rows, evaluate the condition, update last_fired_at, and insert into alert_history — all in one transaction. Add a cooldown column (or rely on last_fired_at < NOW() - INTERVAL '1 hour') in the WHERE clause before any notification is sent. The SKIP LOCKED prevents the second overlapping execution from processing the same rows.
Stock price data goes stale at night and on weekends, triggering false alerts
Symptom: The Finnhub /quote endpoint returns the most recent available price even when markets are closed. If a user sets an alert for AAPL below $180 and the market closes at $179, the polling engine returns $179 repeatedly through the weekend. Without a market-hours check, the alert evaluation worker fires the same notification every hour until the market reopens.
Fix: Before running alert evaluation, call the Finnhub /market-status endpoint to check whether US markets are currently open. Skip the evaluation entirely when markets are closed. Store a checked_at timestamp per polling run and add a guard in the evaluation query: WHERE sp.updated_at > NOW() - INTERVAL '15 minutes'. This ensures stale cached prices can never trigger alerts.
Browser push subscriptions stop working after 30 days for some users
Symptom: Web Push subscription endpoints issued by browsers (Chrome, Firefox, Edge) can expire or become invalid when the user reinstalls the browser, clears site data, or the browser updates its push service registration. The push_subscriptions table accumulates stale rows, and the Edge Function keeps attempting delivery to expired endpoints that silently fail or return HTTP 410 Gone.
Fix: After every push delivery attempt, check the HTTP response status. On 410 Gone or 404 Not Found, delete the corresponding row from push_subscriptions immediately. Re-register the push subscription on every page load using the ServiceWorkerRegistration.pushManager.subscribe() call with an upsert into push_subscriptions. This keeps the table fresh and prevents silent delivery failures.
Finnhub API returns 429 errors in the Vercel Cron or Edge Function
Symptom: The Finnhub free tier allows 60 API calls per minute. If your app has users watching 80 unique tickers, a single cron execution that fires 80 sequential /quote requests will hit the rate limit around call 60 and fail for the remaining tickers. The tickers that fail don't get price updates, so alerts on those tickers may miss a trigger window.
Fix: Group ticker requests into batches of 50 with a 1-second sleep between batches. Cache results in the stock_prices table so subsequent evaluation runs read from the cache instead of making additional API calls. Upgrade to a Finnhub paid plan if you consistently have more than 50 unique tickers active. Log 429 errors with the ticker and timestamp so you can detect which tickers are frequently rate-limited.
Best practices
Validate ticker symbols against the data API before saving an alert — a typo in APPL (instead of AAPL) will silently never trigger
Share the price polling across users: one Finnhub call per unique ticker serves all users watching that ticker, keeping API costs proportional to ticker diversity rather than user count
Add a market-hours check before alert evaluation to prevent stale overnight prices from generating noise notifications
Set a minimum cooldown of 1 hour between repeated fires of the same alert — wrap the condition check and last_fired_at update in a single atomic transaction
Re-register push subscriptions on every page load with an upsert — subscription endpoints expire silently and must be refreshed proactively
Log every alert evaluation run with ticker, price, condition result, and whether a notification was sent — essential for debugging user complaints about missed or duplicate alerts
Show users the data latency prominently in the UI ('prices delayed up to 5 minutes') to set expectations before they contact support wondering why their alert didn't fire instantly
When You Need Custom Development
V0 and Lovable handle polling-based price alerts well. These requirements break the pattern:
- Sub-second latency is required — real-time WebSocket streaming from Polygon.io replaces polling and requires a persistent server process, not a serverless cron job
- Complex alert conditions like RSI crossover, moving average breach, or MACD signal require a time-series price buffer and indicator calculation logic that AI tools won't generate accurately
- Regulatory or fintech licensing requirements mandate an audit trail with data residency and access logging for every price fetch and alert decision
- Multi-asset coverage beyond US equities — options chains, futures, crypto derivatives — each requiring a different data vendor and different contract models
RapidDev builds these features for production
Real auth, database, and payments — fixed price, delivered in weeks. You own 100% of the code.
Frequently asked questions
How often does the app check stock prices?
With a polling architecture (the standard AI-built approach), prices are checked every 1–5 minutes via a Vercel Cron Job or Supabase pg_cron. This means alerts can fire up to 5 minutes after the condition is actually met. For sub-second latency, you need a real-time WebSocket connection to a provider like Polygon.io — that's a custom development project, not something AI tools generate reliably.
Can I set alerts for cryptocurrency?
Not with Finnhub's free tier, which covers US equities. For crypto, use the CoinGecko API (free, 30 calls/minute) or CryptoCompare. The same polling architecture applies — swap the Finnhub /quote call for CoinGecko's /simple/price endpoint. The database schema and alert evaluation logic are identical; only the data source changes.
What happens if I miss a notification?
The alert history table records every trigger event with the price at the time of firing, so users can always check the dashboard to see when an alert fired and what price triggered it. Once an alert fires, it respects the 1-hour cooldown before firing again. If the user wants to reset the cooldown, they can pause and resume the alert from the dashboard.
Is there a limit on how many alerts I can create?
There's no hard technical limit in the database. The practical limit is API rate budget: if one user sets 100 alerts across 100 different tickers, each unique ticker requires a separate API call in your polling loop. With Finnhub's free tier (60 calls/minute), a single user with 100 unique tickers could consume your entire rate budget. Consider setting a per-user limit (20–50 alerts) and caching prices aggressively by batching shared tickers.
How do I stop alerts from firing after market hours?
Add a market hours check before the alert evaluation step. Finnhub provides a /market-status endpoint that returns whether US markets are currently open. Call it once at the start of each evaluation run and skip evaluation entirely when markets are closed. This prevents stale overnight prices from triggering alerts on already-met conditions.
Can alerts send to Slack or Telegram instead of email?
Yes — replace the Resend email call in the Edge Function with a Slack webhook POST (free, Incoming Webhooks app) or Telegram Bot API message (free). The notification sender is just an HTTP call with the alert payload; you can add multiple channels in parallel. Add a user preference table column for notification_channel (email/push/slack/telegram) and store the relevant connection string per user.
How accurate is the price data?
Accuracy depends on the tier. Finnhub free provides real-time US equity quotes during market hours with a potential 15-second delay for some symbols. Alpha Vantage free provides data that can lag 15+ minutes. Polygon.io Stocks Starter ($79/mo) delivers genuinely real-time data with sub-second latency. For an alert app where a $1 move matters, display the data source and timestamp on each alert so users know what they're comparing against.
What API do I need to subscribe to for real-time prices?
For most alert apps, Finnhub's free tier (60 calls/minute) is sufficient at launch. Upgrade to a paid Finnhub plan or switch to Polygon.io Stocks Starter ($79/mo approx) when you need reliable real-time data or have more than 60 unique tickers active at once. Polygon.io is the standard choice for apps where price accuracy directly drives user decisions.
Need this feature production-ready?
RapidDev builds stock alerts into real apps — auth, database, payments — at $13K–$25K.
Book a free consultation30-min call. No commitment.