AI-Powered Cyber Threat Intelligence for small and mid-sized organizations.
ThreatPulse pulls threat data from CISA, NVD, and major security feeds, has Claude AI write it up in plain English, and delivers it to subscribers through a dashboard, library, AI chat advisor, and email alerts. Free, $39/mo Pro, and $199/mo Enterprise tiers.
- What you need before starting
- Load this project into Antigravity IDE
- Create a GitHub repository
- Sign up for the three required services
- Fill in your
.envfile - Run ThreatPulse on your computer (Docker — easiest)
- Or run without Docker (manual)
- Test that everything works
- Deploy to production
- Schedule the weekly email digest
- Troubleshooting
- Project layout
A laptop or desktop with internet access, plus these free tools installed:
| Tool | What it is | Where to get it |
|---|---|---|
| Git | Version control. Required to push code to GitHub. | git-scm.com/downloads |
| Docker Desktop | Runs the app in a sealed container. The fastest way to start. | docker.com/products/docker-desktop |
| Antigravity IDE | The editor you're using to read this. | (you already have it) |
| A web browser | For signing up at the service websites. | (you already have one) |
You will also create accounts at four free services later in this guide:
- GitHub — to host your code
- Anthropic — provides the Claude AI
- Resend — sends emails
- Stripe — takes credit card payments
You do not need a credit card to test ThreatPulse. Stripe has a "test mode" with fake cards. Anthropic and Resend both have free tiers.
- Unzip
threatpulse.zipsomewhere easy to find, like yourDocumentsfolder. You should now have a folder calledthreatpulse. - Open Antigravity IDE.
- Click File → Open Folder (or the equivalent in your IDE).
- Pick the
threatpulsefolder you just unzipped. Click Open. - The file tree on the left should now show folders like
backend/,frontend/,scripts/, and files likeREADME.md. You're in.
You'll push your copy of ThreatPulse to a private GitHub repository so you can deploy it later and keep a backup.
- Go to github.com and click Sign up.
- Pick a username, email, and password. Verify your email.
- In the top-right of GitHub, click the + icon → New repository.
- Repository name:
threatpulse - Description:
AI-powered cyber threat intelligence platform(optional) - Visibility: choose Private (recommended).
- Do NOT check "Add a README", "Add .gitignore", or "Choose a license" — your local copy already has those.
- Click Create repository.
- The next page shows a URL like
https://github.com/YOUR-USERNAME/threatpulse.git. Copy that URL — you'll paste it in the next step.
Open a terminal inside Antigravity IDE (usually View → Terminal or Terminal → New Terminal). Make sure the terminal is in the threatpulse folder. Then run these commands one at a time:
git init
git add .
git commit -m "Initial commit of ThreatPulse"
git branch -M main
git remote add origin https://github.com/YOUR-USERNAME/threatpulse.git
git push -u origin mainReplace YOUR-USERNAME with your actual GitHub username. You may be asked to log in to GitHub — follow the prompts. When git push finishes without errors, refresh your GitHub repo page in the browser. You should see all the files.
ThreatPulse needs three external services to be fully functional. You can skip any of these and the app will still run, but the matching feature won't work.
- Go to console.anthropic.com and sign up.
- Add at least $5 of credit (you'll pay as you use it; small testing only costs cents).
- In the left menu, click API Keys → Create Key.
- Name it
threatpulse. Copy the key (starts withsk-ant-...). You won't see it again — paste it somewhere safe for now.
- Go to resend.com and sign up. Free tier sends 3,000 emails/month.
- Verify your account email when they send you a confirmation link.
- In Resend, go to Domains → Add Domain. Enter a domain you own (e.g.,
yourcompany.com). - Resend will show a list of DNS records to add. Log in to wherever you registered your domain (GoDaddy, Namecheap, Cloudflare, etc.) and add those records exactly as shown.
- Wait 5–60 minutes, then click Verify in Resend. You need green checkmarks before email will send.
- Don't own a domain? Resend lets you send from
onboarding@resend.devwhile testing — skip steps 3–5 and use that as yourEMAIL_FROMlater. - Go to API Keys → Create API Key. Name it
threatpulse. Choose Sending access. Copy the key (starts withre_...).
- Go to stripe.com and sign up.
- Stay in Test Mode while you're getting started — there's a toggle in the top-right of the dashboard. Test mode uses fake cards and costs nothing.
- Click Developers → API Keys. You'll see two keys:
- A Publishable key that starts with
pk_test_... - A Secret key that starts with
sk_test_...(click reveal)
- A Publishable key that starts with
- Copy both. Paste them somewhere safe.
- Now create the Pro and Enterprise products. The project includes a script that does this for you. Open a terminal in your
threatpulsefolder and run:cd backend pip install stripe STRIPE_SECRET_KEY=sk_test_paste_yours_here python ../scripts/setup_stripe.py - The script will print two lines like:
Copy both. You'll paste them into
STRIPE_PRICE_PRO=price_1AbCd... STRIPE_PRICE_ENTERPRISE=price_1XyZw....envnext.
Webhook secret comes later — you'll set that up after the app is running, in section 8.4.
The .env file holds every secret key. It's the heart of your configuration.
In a terminal, in the threatpulse folder:
cp .env.example .env(On Windows PowerShell: copy .env.example .env)
JWT_SECRET is the secret key that signs login tokens. The app will refuse to start if this is empty or set to the default — that's intentional, to prevent accidentally deploying with an unsafe value.
In a terminal:
openssl rand -hex 32This prints a long random string. Copy it.
(No openssl? On Windows you can also use Git Bash, or just go to random.org/strings and grab any 64-character hex string.)
Open .env in Antigravity IDE and paste each value into the matching slot:
# Database — leave as is for local Docker; replace for production
DATABASE_URL=postgresql://threatpulse:threatpulse_dev@localhost:5432/threatpulse
# Auth — paste your random hex string here
JWT_SECRET=paste-your-openssl-output-here
JWT_ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=1440
# Anthropic — your Claude API key
ANTHROPIC_API_KEY=sk-ant-paste-here
# Stripe — your test keys + the price IDs from step 4.3
STRIPE_SECRET_KEY=sk_test_paste-here
STRIPE_WEBHOOK_SECRET= # Leave blank for now
STRIPE_PRICE_PRO=price_paste-here
STRIPE_PRICE_ENTERPRISE=price_paste-here
# Resend
RESEND_API_KEY=re_paste-here
EMAIL_FROM=alerts@yourdomain.com # Use onboarding@resend.dev if no verified domain
# URLs
FRONTEND_URL=http://localhost:3000
CORS_ORIGINS=http://localhost:3000
# Frontend (Next.js reads NEXT_PUBLIC_* values)
NEXT_PUBLIC_API_URL=http://localhost:8000
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_paste-here
NEXT_PUBLIC_STRIPE_PRICE_PRO=price_paste-here # Same value as STRIPE_PRICE_PRO
NEXT_PUBLIC_STRIPE_PRICE_ENTERPRISE=price_paste-here # Same value as STRIPE_PRICE_ENTERPRISE
# Scraper schedule
SCRAPE_CISA_INTERVAL_HOURS=1
SCRAPE_NVD_INTERVAL_HOURS=4
SCRAPE_VENDOR_INTERVAL_HOURS=12Save the file.
Important: never commit
.envto GitHub. The included.gitignorealready excludes it, but if you accidentallygit add .env, undo withgit rm --cached .env.
Open Docker Desktop. Wait until the whale icon at the top of the screen says "Docker is running".
In a terminal in the threatpulse folder:
docker-compose up --buildThe first time, this downloads several gigabytes (Postgres, Python, Node.js images) and takes 5–10 minutes. You'll see a lot of build output. When it's done you'll see lines like:
backend_1 | INFO: Uvicorn running on http://0.0.0.0:8000
frontend_1 | ▲ Next.js 14.2.15
frontend_1 | - Local: http://localhost:3000
Leave that terminal running. Open a second terminal for the next steps.
In your second terminal:
docker-compose exec backend python -m app.scrapers.run --seedThis adds 5 demo threats so the dashboard isn't empty. You should see Seeded 5 threats..
In your browser, go to http://localhost:3000. You should see the dashboard with sample threats. Click Sign In at the top right, switch to Sign Up, and create a test account.
Back in the first terminal, press Ctrl+C. To start it again later, just run docker-compose up (no --build needed unless you change code).
Use this route if you don't want to install Docker. You'll need:
- Python 3.11 (python.org)
- Node.js 20 (nodejs.org)
- PostgreSQL 16 (postgresql.org/download)
After installing PostgreSQL, run in a terminal:
createdb threatpulseThen update .env:
DATABASE_URL=postgresql://YOUR_PG_USER:YOUR_PG_PASSWORD@localhost:5432/threatpulseIn one terminal:
cd backend
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt
alembic upgrade head
python -m app.scrapers.run --seed
uvicorn app.main:app --reload --port 8000In a second terminal:
cd frontend
npm install
npm run devBrowse to http://localhost:3000.
- Go to http://localhost:3000.
- Click Sign In → Sign Up.
- Use any email and a password of 8+ characters.
- After signup, the top-right shows your tier as
FREE.
You should see threat counts, the trending threats list, and an AI insight panel.
Open a terminal in the threatpulse folder:
docker-compose exec backend python -c "
from app.services.email_service import send_threat_alert
from app.models.user import User
u = User(email='YOUR-REAL-EMAIL@example.com', full_name='Test')
send_threat_alert(u, 'Test Threat', 'critical', 'This is a test email from ThreatPulse.')
print('Email sent.')
"(If running manually without Docker: cd backend && source venv/bin/activate then run the python code without the docker-compose exec backend prefix.)
If it lands in your inbox, email works. If you get an error, double-check your RESEND_API_KEY and EMAIL_FROM in .env.
For the Subscribe button to actually upgrade users after they pay, Stripe needs to send notifications to your backend. While running locally, use the Stripe CLI:
- Install Stripe CLI from stripe.com/docs/stripe-cli.
- macOS:
brew install stripe/stripe-cli/stripe - Windows: download the installer from the link above
- Linux: see the link
- macOS:
- Log in:
stripe login(opens your browser to confirm). - In a third terminal, run:
stripe listen --forward-to localhost:8000/api/webhook/stripe
- The CLI prints a line like
Your webhook signing secret is whsec_abc123.... Copy that value. - Paste it into
.envasSTRIPE_WEBHOOK_SECRET=whsec_abc123.... - Restart the backend (in Docker:
docker-compose restart backend; manually: Ctrl+C, thenuvicorn app.main:app --reload --port 8000).
- While logged in, click Pricing → Start Pro Trial.
- You'll land on Stripe's checkout page. Use test card:
- Card number:
4242 4242 4242 4242 - Expiry: any future date (e.g.,
12/30) - CVC: any 3 digits (e.g.,
123) - ZIP: any (e.g.,
10001)
- Card number:
- Click Subscribe. You should redirect back to the dashboard.
- Refresh the page — your tier badge should now say
PRO. - Visit a playbook page — the remediation steps should no longer be blurred out.
If the tier doesn't update, check the Stripe CLI terminal for errors. Most common: webhook secret mismatch.
These are the recommended free-tier services. Total cost: about $5–25/month depending on usage.
| Layer | Service | Free tier | Why |
|---|---|---|---|
| Database | Supabase | 500 MB free | Managed Postgres, easy URL |
| Backend | Railway | $5/mo Hobby | Auto-deploys from GitHub, supports Dockerfile |
| Frontend | Vercel | Free | Built for Next.js |
- Sign up at supabase.com.
- Click New Project. Name it
threatpulse. Pick a region close to you. Set a strong database password and save it somewhere safe. - Wait 2 minutes while the database provisions.
- In your project, click Settings (gear icon) → Database → Connection string → URI.
- Copy the URI (it looks like
postgresql://postgres:PASSWORD@db.xxxxx.supabase.co:5432/postgres). Replace[YOUR-PASSWORD]with the password you just set.
- Sign up at railway.app — sign in with GitHub for easiest setup.
- Click New Project → Deploy from GitHub Repo → pick your
threatpulserepo. - Railway will scan and find the Dockerfile. It will ask which folder — pick
backend. - Click Variables in your service and add every variable from your
.envfile:DATABASE_URL→ use the Supabase URL from step 9.1JWT_SECRET,ANTHROPIC_API_KEY,STRIPE_SECRET_KEY, all the othersFRONTEND_URL→ leave ashttp://localhost:3000for now; we'll update after VercelCORS_ORIGINS→ same; update after Vercel
- Click Settings → Networking → Generate Domain. Copy the URL it gives you (like
threatpulse-backend.up.railway.app). - Open Railway's shell (top-right ⋯ menu → Open Shell) and run:
alembic upgrade head python -m app.scrapers.run --seed
- In the same Railway project, click New → Empty Service → connect to the same GitHub repo,
backendfolder. - Copy all the same variables as the backend service.
- Settings → Start Command → set it to:
python -m app.scrapers.run --watch - Deploy. This service runs forever, scraping CISA hourly, NVD every 4h, RSS every 12h.
- Sign up at vercel.com with GitHub.
- Add New Project → import your
threatpulserepo. - Set Root Directory to
frontend. - Environment Variables — add:
NEXT_PUBLIC_API_URL→https://YOUR-RAILWAY-URL(from step 9.2)NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY→ yourpk_test_...(orpk_live_...when going live)NEXT_PUBLIC_STRIPE_PRICE_PRO→ your Pro price IDNEXT_PUBLIC_STRIPE_PRICE_ENTERPRISE→ your Enterprise price ID
- Click Deploy. Wait 2 minutes.
- Vercel gives you a URL like
threatpulse-abc123.vercel.app. Copy it.
Now go back to Railway → backend service → Variables and update:
FRONTEND_URL=https://threatpulse-abc123.vercel.app
CORS_ORIGINS=https://threatpulse-abc123.vercel.app
Click Deploy to apply. Do the same for the scraper service if needed.
- In Stripe dashboard, switch off Test mode (top-right toggle) when you're ready to charge real money. Get your live
sk_live_...andpk_live_...keys. - Go to Developers → Webhooks → Add endpoint.
- URL:
https://YOUR-RAILWAY-URL/api/webhook/stripe - Events: check
checkout.session.completed,customer.subscription.updated,customer.subscription.deleted. - Click Add endpoint. Copy the Signing secret (starts with
whsec_...). - In Railway, update
STRIPE_WEBHOOK_SECRETto that value.
- In Vercel: Settings → Domains → Add your domain (e.g.,
threatpulse.io). - Add the DNS records Vercel shows you at your domain registrar.
- Update Railway's
FRONTEND_URLandCORS_ORIGINSto your custom domain.
The digest script exists at scripts/weekly_digest.py but doesn't run on a schedule by default.
Easy option — Railway cron:
- Railway dashboard → your backend service → Settings → Cron Schedule.
- Cron expression:
0 13 * * 1(Mondays at 1pm UTC = 9am Eastern). - Command:
python -m scripts.weekly_digest - Save.
Alternative — your own server:
crontab -e
# Add this line, replacing /path/to/threatpulse:
0 13 * * 1 cd /path/to/threatpulse && python -m scripts.weekly_digestdocker-compose up fails with "JWT_SECRET is missing or set to the default value"
You forgot to fill JWT_SECRET in .env. See step 5.2.
Email test returns "Invalid API key"
Re-check RESEND_API_KEY in .env. Be sure there are no quotes or trailing spaces. If you used a domain you didn't fully verify, set EMAIL_FROM=onboarding@resend.dev.
Stripe checkout button does nothing
Check the browser dev console (F12). If it says priceId is empty, you forgot to set NEXT_PUBLIC_STRIPE_PRICE_PRO in .env and restart the frontend.
After paying with the test card, my tier stays "free"
The webhook isn't reaching the backend. Locally: make sure stripe listen is running and STRIPE_WEBHOOK_SECRET matches its output. In production: check Stripe Dashboard → Developers → Webhooks → click your endpoint → see the Recent attempts log; failed attempts will show why.
Dashboard is empty / "Loading threats..." never finishes Run the seed command:
docker-compose exec backend python -m app.scrapers.run --seedOr wait for the scraper service to pick up real threats (first run may take 5–10 minutes).
alembic upgrade head fails with "relation already exists"
You've previously created tables. Reset:
docker-compose down -v # WARNING: deletes the database
docker-compose up --buildCORS errors in the browser console
Your frontend URL isn't in the backend's CORS_ORIGINS setting. Update it in .env (or in Railway's variables) and restart the backend.
Free-tier user can't use the AI advisor — "Free tier limit reached"
That's by design — 3 questions per day for free, then upgrade to Pro. Your test account hit the limit. Either log in as a Pro user, or wait until tomorrow, or change FREE_TIER_DAILY_LIMIT in backend/app/api/routes.py.
threatpulse/
├── backend/ Python FastAPI server
│ ├── app/
│ │ ├── api/routes.py All HTTP endpoints
│ │ ├── core/
│ │ │ ├── auth.py JWT login + tier guards
│ │ │ ├── config.py Settings, env loading
│ │ │ └── database.py SQLAlchemy setup
│ │ ├── models/user.py Database tables
│ │ ├── schemas/schemas.py Request/response shapes
│ │ ├── services/
│ │ │ ├── ai_service.py Talks to Claude
│ │ │ ├── email_service.py Talks to Resend
│ │ │ └── stripe_service.py Talks to Stripe
│ │ ├── scrapers/
│ │ │ ├── collectors.py Scrapes CISA, NVD, RSS feeds
│ │ │ └── run.py Scheduler + seed data
│ │ └── main.py FastAPI app entry
│ ├── alembic/ Database migrations
│ ├── Dockerfile
│ └── requirements.txt
├── frontend/ Next.js 14 web app
│ ├── src/
│ │ ├── app/
│ │ │ ├── dashboard/ Main dashboard
│ │ │ ├── library/ Searchable threat list
│ │ │ ├── playbook/[slug]/ Threat detail + AI advisor
│ │ │ ├── pricing/ Subscription tiers
│ │ │ └── layout.tsx
│ │ ├── components/ui/Nav.tsx Top nav + sign-in modal
│ │ ├── lib/
│ │ │ ├── api.ts Calls the backend
│ │ │ └── auth.tsx Login state
│ │ └── types/index.ts TypeScript types
│ ├── Dockerfile
│ └── package.json
├── scripts/
│ ├── deploy.sh Step-by-step deployment notes
│ ├── setup_stripe.py Creates Stripe products
│ └── weekly_digest.py Sends Monday digest emails
├── docker-compose.yml Local dev stack
├── .env.example Template for your secrets
└── README.md You are here
MIT. See LICENSE.
This project is provided as-is. For issues or questions, open an issue in the GitHub repository you created in section 3.