# How to Add Stock Alerts to Your App (Copy-Paste Prompts Included)

- Tool: App Features
- Last updated: July 2026

## TL;DR

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.

## 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** (ui): Ticker 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.
- **Ticker Lookup Service** (service): Finnhub /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.
- **Price Polling Engine** (backend): A 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.
- **Alert Evaluation Worker** (backend): SQL 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.
- **Browser Push Notification** (service): Web 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.
- **Email Notification** (service): Resend 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.
- **Alerts Dashboard** (ui): A 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.

## 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.

```sql
create table public.stock_alerts (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references auth.users(id) on delete cascade not null,
  ticker text not null,
  condition text not null check (condition in ('above','below','pct_up','pct_down')),
  threshold numeric not null,
  last_fired_at timestamptz,
  is_active bool not null default true,
  created_at timestamptz not null default now()
);

create table public.stock_prices (
  ticker text primary key,
  price numeric not null,
  change_pct numeric,
  updated_at timestamptz not null default now()
);

create table public.push_subscriptions (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references auth.users(id) on delete cascade not null,
  endpoint text not null unique,
  p256dh text not null,
  auth text not null,
  created_at timestamptz not null default now()
);

create table public.alert_history (
  id uuid primary key default gen_random_uuid(),
  alert_id uuid references public.stock_alerts(id) on delete cascade not null,
  fired_at timestamptz not null default now(),
  price_at_fire numeric not null
);

alter table public.stock_alerts enable row level security;
alter table public.stock_prices enable row level security;
alter table public.push_subscriptions enable row level security;
alter table public.alert_history enable row level security;

create policy "Users manage own alerts"
  on public.stock_alerts for all
  using (auth.uid() = user_id)
  with check (auth.uid() = user_id);

create policy "Service role manages stock prices"
  on public.stock_prices for all
  using (auth.role() = 'service_role');

create policy "Users manage own push subscriptions"
  on public.push_subscriptions for all
  using (auth.uid() = user_id)
  with check (auth.uid() = user_id);

create policy "Users view own alert history"
  on public.alert_history for select
  using (
    exists (
      select 1 from public.stock_alerts sa
      where sa.id = alert_id and sa.user_id = auth.uid()
    )
  );

create policy "Service role inserts alert history"
  on public.alert_history for insert
  with check (auth.role() = 'service_role');

create index stock_alerts_ticker_active_idx
  on public.stock_alerts (ticker)
  where is_active = true;

create index stock_alerts_user_idx
  on public.stock_alerts (user_id, created_at desc);
```

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 paths

### V0 — fit 4/10, 3–5 hours

Best path for a professional finance UI: V0 generates clean Next.js App Router components with API routes for Finnhub polling and a Resend email action. The alerts dashboard UI quality is best-in-class.

1. Prompt V0 with the spec below to generate the alerts dashboard, ticker search component, and API routes
2. In the V0 Vars panel, add FINNHUB_API_KEY, RESEND_API_KEY, NEXT_PUBLIC_SUPABASE_URL, and NEXT_PUBLIC_SUPABASE_ANON_KEY
3. Run the SQL schema above in the Supabase SQL editor to create all four tables and RLS policies
4. Add a vercel.json to your project with the crons config: { "crons": [{ "path": "/api/poll-prices", "schedule": "*/5 * * * *" }] } — V0 generates the API route but not the cron config automatically
5. Publish to Vercel and verify the /api/poll-prices route runs on schedule in the Vercel dashboard under the Crons tab

Starter prompt:

```
Build a stock price alert feature in Next.js App Router using Finnhub as the data source (API key from FINNHUB_API_KEY env var) and Supabase for the database.

Database tables already exist: stock_alerts (id, user_id, ticker, condition text check in above/below/pct_up/pct_down, threshold numeric, last_fired_at, is_active, created_at), stock_prices (ticker, price, change_pct, updated_at), push_subscriptions (id, user_id, endpoint, p256dh, auth), alert_history (id, alert_id, fired_at, price_at_fire).

Build these pages and API routes:
1. /alerts page: dashboard table showing user's alerts with live current price column (SWR 60s refresh), condition badge, threshold, last_fired_at, toggle to enable/disable, delete button
2. AlertCreateModal: ticker search input hitting /api/ticker-search (calls Finnhub symbol/search, debounced 300ms); condition dropdown (Price above / Price below / % up / % down); threshold number input with Zod validation; saves to stock_alerts via Supabase client
3. /api/poll-prices route: fetches all distinct active tickers from stock_alerts, calls Finnhub /quote for each (batch with 1s delay between groups of 50 to respect 60 calls/min limit), upserts into stock_prices
4. /api/evaluate-alerts route: evaluates all active alerts against stock_prices WHERE price meets condition AND (last_fired_at IS NULL OR last_fired_at < NOW() - INTERVAL '1 hour'); for each match: update last_fired_at atomically, insert into alert_history, call Resend API to send email notification with ticker, price_at_fire, threshold, and link to /alerts
5. Push notification opt-in flow on /alerts page: requestPermission on button click, save subscription to push_subscriptions via /api/subscribe route using web-push VAPID keys from env vars

Edge cases: skip alert evaluation when stock_prices.updated_at is more than 15 minutes old (market closed); skip alerts where price is 0 or NULL; handle 410 Gone from push service by deleting the subscription row.
```

Limitations:

- V0 does not generate the vercel.json crons configuration automatically — you must add it manually for the polling job to run on a schedule
- Finnhub free tier is limited to 60 API calls/minute — if users set alerts on more than 60 unique tickers, polling will hit 429 errors without the batching logic specified in the prompt
- Browser push subscriptions must be re-registered on each page load; V0 may not include the upsert logic by default without explicit instruction

### Lovable — fit 3/10, 4–6 hours

Lovable's Supabase integration handles the alerts table, Edge Functions for polling and evaluation, and pg_cron for scheduling. Best when you want the full stack in one place without configuring Vercel separately.

1. Create a new Lovable project and connect Lovable Cloud to provision Supabase auth and the database
2. Paste the prompt below and let Agent Mode scaffold the alerts UI, Edge Functions, and database tables
3. In Lovable Cloud tab → Secrets, add FINNHUB_API_KEY, RESEND_API_KEY, VAPID_PUBLIC_KEY, and VAPID_PRIVATE_KEY
4. Enable pg_cron in Supabase (requires Pro plan, $25/mo) and add the cron job: SELECT cron.schedule('poll-prices', '*/5 * * * *', 'SELECT net.http_post(url := current_setting(''app.edge_function_url'') || ''/poll-prices'', headers := jsonb_build_object(''Authorization'', ''Bearer '' || current_setting(''app.service_key'')))')
5. Publish the app and test alert delivery on the published URL — pg_cron-triggered notifications do not work in the Lovable preview environment

Starter prompt:

```
Build a stock price alert feature using Supabase Edge Functions, pg_cron, and the Finnhub API.

Create these database tables with RLS:
- stock_alerts: id uuid, user_id uuid references auth.users, ticker text, condition text check in (above, below, pct_up, pct_down), threshold numeric, last_fired_at timestamptz, is_active bool default true, created_at timestamptz
- stock_prices: ticker text primary key, price numeric, change_pct numeric, updated_at timestamptz
- push_subscriptions: id uuid, user_id uuid references auth.users, endpoint text unique, p256dh text, auth text
- alert_history: id uuid, alert_id uuid references stock_alerts, fired_at timestamptz, price_at_fire numeric

RLS: authenticated users manage own stock_alerts and push_subscriptions rows; stock_prices only accessible by service_role; alert_history SELECT for own alerts only via JOIN.

Create these Edge Functions:
1. poll-prices: queries distinct active tickers from stock_alerts, calls Finnhub /quote for each (API key from FINNHUB_API_KEY secret), batches 50 tickers per request group with 1s delay, upserts into stock_prices
2. evaluate-alerts: SELECT sa.* FROM stock_alerts sa JOIN stock_prices sp ON sa.ticker = sp.ticker WHERE sa.is_active = true AND sp.updated_at > NOW() - INTERVAL '15 minutes' AND sp.price > 0 AND (sa.last_fired_at IS NULL OR sa.last_fired_at < NOW() - INTERVAL '1 hour') AND CASE WHEN sa.condition = 'above' THEN sp.price >= sa.threshold WHEN sa.condition = 'below' THEN sp.price <= sa.threshold WHEN sa.condition = 'pct_up' THEN sp.change_pct >= sa.threshold WHEN sa.condition = 'pct_down' THEN sp.change_pct <= -sa.threshold END; for each match: update last_fired_at, insert alert_history, send Resend email (RESEND_API_KEY secret) and web push (VAPID keys from secrets)

UI: alerts dashboard with live price column (60s SWR refresh), ticker search with Finnhub symbol validation, alert creation modal, push notification opt-in button that requests permission then saves subscription to push_subscriptions.

Handle edge case: market closed detection by checking stock_prices.updated_at > 15 minutes old before running evaluation.
```

Limitations:

- pg_cron requires Supabase Pro plan ($25/mo) — the free tier has no server-side scheduling
- pg_cron-triggered notifications do not fire in the Lovable preview environment — always test alert delivery on the published URL
- Lovable's preview iframe blocks the browser Notification API, so push subscription opt-in must also be tested on the published URL

### Custom — fit 5/10, 1–2 weeks

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.

1. Polygon.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
2. Redis (Upstash) as a shared price cache so the alert evaluation worker reads from memory rather than hitting the Polygon.io API per evaluation cycle
3. Complex condition engine: RSI crossover, moving average breach, MACD signal — requires a time-series price buffer (pgvector or TimescaleDB) and indicator calculation logic
4. Multi-channel delivery: Twilio SMS for critical alerts, Slack webhook for trader dashboards, in addition to email and browser push

Limitations:

- 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

## Gotchas

- **Alert fires repeatedly every minute instead of once per condition trigger** — 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** — 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** — 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** — 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

## 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.

---

Source: https://www.rapidevelopers.com/app-features/stock-alerts
© RapidDev — https://www.rapidevelopers.com/app-features/stock-alerts
