Stock Alerts is a study project for monitoring a personal list of assets traded on the Brazilian market. It was built to explore the Next.js App Router, feature-oriented application architecture, scheduled background work, external data providers, and email delivery.
The application stores daily market data, calculates exponential moving averages, records technical buy signals, and displays the results in an authenticated dashboard. It is not intended to provide investment advice.
- Screenshots
- Features
- The
SUPERrole - How the monitoring job works
- Hosting
- Architecture
- Project structure
- Scripts
The main screen shows the latest stored price, current EMA-based suggestion, market-data date, and monitoring status for each asset.
Each monitored asset has a detail page with its latest price, EMA 6/13/42 values, a 60-session EMA chart, and an OHLC candlestick chart.
The account email was replaced with email@example.com while capturing these screenshots. The remaining data comes from the running production application.
- Google authentication with an optional exact-email allowlist.
- Profile-owned watchlists with symbol validation through brapi.dev.
- Asset names, logos, notes, and per-asset pause/resume controls.
- Daily OHLC price snapshots and EMA 6, 13, and 42 calculations.
- Current technical classification: buy, hold, sell, or insufficient data.
- Buy-signal history based on EMA crossovers.
- Per-profile email preference and Resend signal digests.
- Protected Vercel Cron routes for market checks and missing-logo refreshes.
- Persisted job history, checkpoints, and email delivery attempts.
- Unit, component, server-action, and Playwright browser tests.
The project includes a small hidden maintainer flow:
- Sign in normally.
- Click the email address in the dashboard header five times within two seconds.
- Enter the value configured in
ROLE_ACCESS_PASSWORD.
The profile is promoted to SUPER and the Execuções section becomes available. Server-side authorization also protects the route and retry action.
The execution screen shows recent job results, duration, processed assets, generated signals, email outcomes, and provider errors. A SUPER profile can retry the latest failed alert-check run.
ROLE_ACCESS_PASSWORD is a shared promotion secret. Use a long, unique value and restrict ALLOWED_EMAILS in production. The promoted role is stored on the application profile.
The scheduled alert check:
- Finds enabled profile/asset pairs.
- Fetches each distinct symbol from brapi.dev once per run.
- Stores new price history and recalculates EMA snapshots.
- Detects buy-signal crossovers for each profile.
- Sends at most one eligible signal digest per profile and market date.
- Records checkpoints, delivery results, and the overall job summary.
Price refreshes request data through the current São Paulo calendar date so the dashboard can store the freshest daily snapshot exposed by the provider. Alert evaluation excludes that date until the next calendar day, preventing an in-progress daily candle from generating a signal. The email date comes from the latest completed snapshot returned for each asset rather than assuming the provider has yesterday's data; delayed snapshots can therefore generate one deduplicated digest when they become available.
The current dashboard suggestion is based on EMA alignment:
| Alignment | Suggestion |
|---|---|
EMA 6 > EMA 13 > EMA 42 |
Buy |
EMA 6 < EMA 13 < EMA 42 |
Sell |
| Any other complete alignment | Hold |
| One or more averages unavailable | Insufficient data |
Recorded buy signals use two crossover rules:
- EMA 6 crosses above EMA 42.
- EMA 6 crosses above EMA 13 while EMA 6 is above EMA 42.
The repository is configured for Vercel and PostgreSQL. Another Node.js host can run the application, but it must provide equivalent scheduled HTTPS requests for the two cron routes.
| Dependency | Requirement |
|---|---|
| Git | Current stable version |
| Node.js | 20.9 or newer |
| pnpm | 11.8.0 |
| PostgreSQL client tools | Recommended for local database management |
Enable the package-manager version declared by the repository:
corepack enable
corepack prepare pnpm@11.8.0 --activateIf Corepack is unavailable in the installed Node.js distribution, install it first with npm install --global corepack.
| Service | Use |
|---|---|
| PostgreSQL | Application data, Auth.js sessions, and job history |
| Google Cloud | OAuth 2.0 web client |
| brapi.dev | B3 asset metadata and historical prices |
| Resend | Signal digest delivery |
| Vercel | Hosting and scheduled route invocation |
The current email-provider validation requires a sender on the exact fellcor.com domain. A fork using another domain must update src/features/alerts/infrastructure/email-delivery-provider-factory.ts.
git clone https://github.com/LeFelps/stock-alerts.git
cd stock-alerts
pnpm install --frozen-lockfileFor a locally running PostgreSQL server:
createdb stock_alertsFor Vercel, provision a managed PostgreSQL database through the Storage Marketplace or another provider. Use a pooled connection string for serverless deployments when the provider offers one.
Use separate databases for production and preview deployments. The prebuild script runs pnpm db:migrate, so each build migrates the database configured for that environment.
In the Google Auth Platform:
- Create or select a project.
- Configure the OAuth consent screen.
- Create an OAuth 2.0 client with type Web application.
- Add the exact callback URLs that will be used:
http://localhost:3000/api/auth/callback/google
https://<your-production-domain>/api/auth/callback/google
Save the client ID and secret as AUTH_GOOGLE_ID and AUTH_GOOGLE_SECRET.
Create a token in the brapi dashboard. Some test symbols work without authentication, but normal production coverage requires BRAPI_API_TOKEN.
For email delivery:
- Add
fellcor.comto the Resend Domains dashboard. - Publish the supplied SPF and DKIM records.
- Wait for the domain status to become Verified.
- Create a project-specific API key with Sending access.
cp .env.example .env.localGenerate separate secrets for Auth.js, cron requests, and role promotion:
openssl rand -base64 32Complete .env.local:
AUTH_SECRET=<random-auth-secret>
AUTH_GOOGLE_ID=<google-client-id>
AUTH_GOOGLE_SECRET=<google-client-secret>
DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/stock_alerts
# Optional. Blank allows any Google account with an email address.
ALLOWED_EMAILS=you@example.com
ROLE_ACCESS_PASSWORD=<random-role-password>
MARKET_DATA_PROVIDER=brapi
BRAPI_API_TOKEN=<brapi-token>
EMAIL_PROVIDER=resend
RESEND_API_KEY=<resend-key-starting-with-re_>
ALERT_EMAIL_FROM="Stock Alerts <noreply.stock-alerts@fellcor.com>"
APP_BASE_URL=http://localhost:3000
CRON_SECRET=<random-cron-secret>| Variable | Requirement |
|---|---|
AUTH_SECRET |
Required by Auth.js |
AUTH_GOOGLE_ID |
Required Google OAuth client ID |
AUTH_GOOGLE_SECRET |
Required Google OAuth client secret |
DATABASE_URL |
Required PostgreSQL connection string |
ALLOWED_EMAILS |
Optional comma/newline-separated allowlist |
ROLE_ACCESS_PASSWORD |
Required to unlock the SUPER role |
MARKET_DATA_PROVIDER |
Optional; defaults to brapi |
BRAPI_API_TOKEN |
Required for normal production coverage |
EMAIL_PROVIDER |
Optional; defaults to resend |
RESEND_API_KEY |
Required for email delivery and must start with re_ |
ALERT_EMAIL_FROM |
Required and currently restricted to fellcor.com |
APP_BASE_URL |
Optional on Vercel; base URL for links in alert emails |
CRON_SECRET |
Required by both scheduled routes |
Environment files are ignored by Git except for .env.example. None of these variables should use a NEXT_PUBLIC_ prefix.
pnpm db:migrate
pnpm devOpen http://localhost:3000 and sign in with Google.
The alert-check route can be invoked manually with the cron secret:
curl --fail-with-body \
--header "Authorization: Bearer $CRON_SECRET" \
http://localhost:3000/api/cron/check-alertsThis performs real provider requests and database writes.
pnpm lint
pnpm format
pnpm test
pnpm buildpnpm build applies migrations first and requires a reachable DATABASE_URL.
For the browser suite:
pnpm exec playwright install
pnpm test:e2e- Import the Git repository into a new Vercel project.
- Connect the production PostgreSQL database and expose its connection string as
DATABASE_URL. - Add the environment variables above to the Production environment.
- If previews are enabled, add their variables separately and use an isolated preview database.
- Deploy the project.
- Add the final production callback URL to the Google OAuth client.
- Confirm both jobs under Project → Settings → Cron Jobs.
- Sign in, add an asset, and invoke the alert-check route once to populate initial history.
- Unlock
SUPERand inspect the resulting execution.
Environment-variable changes apply only to new Vercel deployments, so redeploy after editing them.
vercel.json registers:
| Schedule | Route | Purpose |
|---|---|---|
30 10 * * * |
/api/cron/refresh-asset-logos |
Refresh missing or generic logos |
0 11 * * 2-6 |
/api/cron/check-alerts |
Update prices, indicators, signals, and emails |
Vercel evaluates both schedules in UTC. The alert check runs Tuesday through Saturday at 11:00 UTC, currently 08:00 in São Paulo. Vercel sends Authorization: Bearer <CRON_SECRET> automatically.
The App Router composes the feature modules, while scheduled routes reuse the same application services used by the protected maintainer actions.
flowchart LR
Browser[Authenticated browser] --> Auth[Google OAuth / Auth.js]
Auth --> UI[Next.js App Router]
UI --> Profiles[(Profiles and watchlists)]
Cron[Vercel Cron] --> Routes[Protected cron routes]
Routes --> Job[Alert-check job]
Job --> Brapi[brapi.dev]
Brapi --> Prices[(Price snapshots)]
Prices --> Indicators[EMA 6 / 13 / 42]
Indicators --> Signals[(Profile signals)]
Signals --> Digest[Daily signal digest]
Digest --> Resend[Resend]
Super[SUPER profile] --> Runs[Job history and retry]
Runs --> Job
src/
├── app/ # App Router pages, layouts, actions, and routes
├── components/ui/ # Shared UI primitives
├── db/ # Drizzle client and PostgreSQL schema
├── features/
│ ├── alerts/ # Email digest delivery
│ ├── indicators/ # EMA calculations and persistence
│ ├── jobs/ # Scheduled orchestration and execution history
│ ├── market-data/ # brapi adapter and market UI
│ ├── profiles/ # Product profiles and preferences
│ ├── role-access/ # SUPER promotion flow
│ ├── signals/ # Signal detection and history
│ └── watchlist/ # Watchlist and asset catalog behavior
└── lib/ # Shared helpers and authentication policies
See docs/architecture.md and docs/adr/ for the architectural rules and accepted decisions.
| Command | Description |
|---|---|
pnpm dev |
Start the development server |
pnpm build |
Migrate and build for production |
pnpm start |
Start the production build |
pnpm lint |
Run ESLint |
pnpm format |
Check formatting |
pnpm test |
Run Vitest once |
pnpm test:e2e |
Run Playwright tests |
pnpm db:generate |
Generate a Drizzle migration |
pnpm db:migrate |
Apply checked-in migrations |
pnpm db:push |
Push the schema directly |


