Practical Application: Automated Data Fetching and Notification Sending
In previous chapters, we learned about various scheduling "carriers" (whether Linux hosts, GitHub Actions, or Vercel Cron). Now, we'll consolidate these capabilities to implement a real-world business scenario: a "Daily Subscription Push System."
Scenario Overview
We'll build a "Daily Crypto Weather Forecast" system. This program will perform three tasks every morning at 8 AM:
- Read all subscribers (Line Notify Tokens) from a free Supabase database.
- Call an external API to fetch the latest Bitcoin price.
- Use a loop to automatically send the latest price to all subscribers via Line Notify.
You can run this script on GitHub Actions or write it as a Next.js API deployed on Vercel. Here, we'll use a Node.js (TypeScript) script as an example.
Prerequisites
- Supabase Database: Create a table named
subscriberswith two columns:id(primary key) andline_token(string). - Line Notify: Visit the Line Notify official website, apply for and generate your personal access token. Manually add this token to the Supabase database.
Core Implementation Code
First, install the necessary packages:
npm install @supabase/supabase-js axios
Hereโs the powerful scheduling script daily-job.ts:
import { createClient } from '@supabase/supabase-js';
import axios from 'axios';
// ==========================================
// 1. Environment Variable Setup
// ==========================================
// In a real environment, use process.env to read theseโnever hardcode them!
const SUPABASE_URL = process.env.SUPABASE_URL || 'https://your-project.supabase.co';
const SUPABASE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY || 'your-service-role-key';
// Initialize Supabase client (using Service Role Key to bypass RLS and read all data)
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
// ==========================================
// 2. Function to Fetch Bitcoin Price
// ==========================================
async function getBitcoinPrice(): Promise<number | null> {
try {
const response = await axios.get('https://api.coindesk.com/v1/bpi/currentprice.json');
const priceStr = response.data.bpi.USD.rate;
// Remove thousand separators and convert to a number
return parseFloat(priceStr.replace(/,/g, ''));
} catch (error) {
console.error('Failed to fetch price:', error);
return null;
}
}
// ==========================================
// 3. Function to Send Line Notify Messages
// ==========================================
async function sendLineNotify(token: string, message: string) {
try {
await axios.post(
'https://notify-api.line.me/api/notify',
`message=${encodeURIComponent(message)}`,
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': `Bearer ${token}`
}
}
);
console.log('โ
Message sent successfully');
} catch (error) {
console.error('โ Failed to send message:', error);
}
}
// ==========================================
// 4. Main Scheduling Logic (Main Job)
// ==========================================
async function runDailyJob() {
console.log('๐ Starting daily scheduled task...');
// Step A: Fetch the latest data
const price = await getBitcoinPrice();
if (!price) {
console.log('Failed to fetch price. Aborting task.');
return;
}
const message = `\nGood morning! ๐ฐ\nToday's Bitcoin price is: $${price} USD\nHappy investing!`;
console.log('Prepared message:', message);
// Step B: Retrieve all subscribers from the database
const { data: subscribers, error } = await supabase
.from('subscribers')
.select('line_token');
if (error || !subscribers) {
console.error('Failed to read subscriber database:', error);
return;
}
console.log(`Found ${subscribers.length} subscribers. Starting broadcast...`);
// Step C: Loop through and send messages
for (const sub of subscribers) {
if (sub.line_token) {
// Add a small delay to avoid hitting Line's rate limit
await new Promise(resolve => setTimeout(resolve, 500));
await sendLineNotify(sub.line_token, message);
}
}
console.log('๐ Daily scheduled task completed!');
}
// Start the program
runDailyJob();
Practical Development Pitfalls to Avoid
When deploying this script to GitHub Actions or a server, beware of these three common industry pitfalls:
-
API Rate Limit: As demonstrated in line 73 of the code, if your subscribers grow from 5 to 5000, and you use
Promise.allto send 5000 requests simultaneously to Line's servers, your API token will be flagged as a DDoS attack and permanently banned. Adding a shortsleepin the loop or using a queue is essential for scheduling scripts. -
Supabase Service Role Key: Since scheduling scripts run automatically on the backend without user context, we can't use the standard
ANON_KEY(it will be blocked by RLS). Instead, we must use the all-powerfulSERVICE_ROLE_KEY. This key is extremely sensitiveโnever expose it or include it in frontend code. Always store it in environment variables! -
Error Handling (Try-Catch) and Retry Mechanisms: Automated scripts run unattended. If Coindesk's API is down, your script will crash. Ensure all network requests are wrapped in
try-catchblocks. Consider adding logic to alert yourself (e.g., via email) if tasks fail, so youโre aware of issues the next morning.
Conclusion
Congratulations! By now, youโve mastered the essence of "automated scheduling." From basic Linux Crontab to cloud-era tools like GitHub Actions and Vercel Cron, youโve built a daily push system with real business value.
Combine these techniques to create automated ticket-buying bots, database backup scripts, or even a one-person newsletter platform. This is the magic of programming: write code once, and let computers work for you forever!
Real-World Project: Daily Newsletter
Combine cron-job.org + GitHub Actions to send a free daily newsletter.
Architecture
cron-job.org (every day at 8 AM)
โ pings Vercel API endpoint
โ triggers GitHub Actions workflow
โ generates newsletter content
โ sends via free email API (SendGrid, Resend)
API Endpoint
# api/newsletter.py
from fastapi import FastAPI, Header, HTTPException
import os
import requests
app = FastAPI()
SECRET = os.getenv("CRON_SECRET")
@app.post("/api/send-newsletter")
async def send_newsletter(authorization: str = Header(None)):
if authorization != f"Bearer {SECRET}":
raise HTTPException(status_code=401, detail="Unauthorized")
# Fetch today's content
articles = fetch_trending_articles()
# Generate HTML
html = render_newsletter_template(articles)
# Send via Resend (free tier: 100 emails/day)
resend_api = "https://api.resend.com/emails"
headers = {
"Authorization": f"Bearer {os.getenv('RESEND_KEY')}",
"Content-Type": "application/json"
}
payload = {
"from": "newsletter@yourdomain.com",
"to": os.getenv("SUBSCRIBERS").split(","),
"subject": f"Daily Digest - {datetime.now().strftime('%B %d, %Y')}",
"html": html
}
response = requests.post(resend_api, json=payload, headers=headers)
return {"status": "sent", "recipients": len(payload["to"])}
Real-World Project: Database Backup
Backup Script (Python)
# scripts/backup.py
import subprocess
import boto3
from datetime import datetime
import os
def backup_postgres():
# Dump database
db_url = os.getenv("DATABASE_URL")
filename = f"backup-{datetime.now().strftime('%Y%m%d-%H%M%S')}.sql"
subprocess.run([
"pg_dump", db_url,
"--no-owner",
"-f", filename
], check=True)
# Upload to S3
s3 = boto3.client(
"s3",
aws_access_key_id=os.getenv("AWS_KEY"),
aws_secret_access_key=os.getenv("AWS_SECRET")
)
s3.upload_file(filename, os.getenv("S3_BUCKET"), f"backups/{filename}")
# Cleanup local file
os.remove(filename)
print(f"Backup uploaded: {filename}")
if __name__ == "__main__":
backup_postgres()
GitHub Actions Workflow
name: Database Backup
on:
schedule:
- cron: '0 4 * * *' # Daily at 4 AM UTC
jobs:
backup:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install Dependencies
run: pip install boto3 psycopg2-binary
- name: Run Backup
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
AWS_KEY: ${{ secrets.AWS_ACCESS_KEY }}
AWS_SECRET: ${{ secrets.AWS_SECRET_KEY }}
S3_BUCKET: ${{ secrets.S3_BUCKET }}
run: python scripts/backup.py
Real-World Project: Uptime Monitoring
Health Check with Alerting
# scripts/uptime.py
import requests
import os
def check_sites():
sites = os.getenv("MONITORED_SITES", "").split(",")
for site in sites:
try:
response = requests.get(site, timeout=10)
if response.status_code != 200:
send_alert(f"{site} returned {response.status_code}")
print(f"โ {site}: {response.status_code}")
else:
print(f"โ
{site}: OK")
except Exception as e:
send_alert(f"{site} failed: {e}")
print(f"โ {site}: {e}")
def send_alert(message):
# Send to Slack webhook
webhook = os.getenv("SLACK_WEBHOOK")
if webhook:
requests.post(webhook, json={"text": message})
# Or send email via SendGrid
# Or send Telegram message
if __name__ == "__main__":
check_sites()
Tool Selection Guide
| Scenario | Best Tool | Why | |----------|-----------|-----| | Ping a URL to prevent sleep | cron-job.org | Free, simple, no code needed | | Run GitHub repository tasks | GitHub Actions | Direct repo access, unlimited public | | Scheduled tasks in Next.js | Vercel Cron Jobs | Same environment as your API | | Complex multi-step workflows | GitHub Actions | Rich ecosystem of actions | | Low-frequency daily tasks | Any | Pick what's already in your stack | | High-frequency (minutes) | cron-job.org | Vercel free tier limited to 1/day |
Summary
Real-world automation combines multiple free tools to build powerful scheduled systems โ newsletters, backups, monitoring, keep-alive, and more โ all without spending anything on servers.
Key takeaways:
- Combine cron-job.org + GitHub Actions + Vercel Cron for full coverage |
- Daily newsletters: cron-job.org triggers API โ sends email |
- Database backups: GitHub Actions + pg_dump + S3 |
- Uptime monitoring: Python script on GitHub Actions with Slack alerts |
- Use the right tool: simple ping โ cron-job.org, complex โ GitHub Actions |
- Always authenticate with secrets and tokens |
- Log and alert on failures for reliable automation |
- Start small, then layer tools together as needs grow |
You've completed this course! Now go build your own automation systems.