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:

  1. Read all subscribers (Line Notify Tokens) from a free Supabase database.
  2. Call an external API to fetch the latest Bitcoin price.
  3. 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

  1. Supabase Database: Create a table named subscribers with two columns: id (primary key) and line_token (string).
  2. 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:

  1. API Rate Limit: As demonstrated in line 73 of the code, if your subscribers grow from 5 to 5000, and you use Promise.all to send 5000 requests simultaneously to Line's servers, your API token will be flagged as a DDoS attack and permanently banned. Adding a short sleep in the loop or using a queue is essential for scheduling scripts.

  2. 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-powerful SERVICE_ROLE_KEY. This key is extremely sensitiveโ€”never expose it or include it in frontend code. Always store it in environment variables!

  3. 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-catch blocks. 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.

Unlock Full Tutorial

This chapter is paid content. Join the project to unlock over 5000 words of deep analysis, including 10+ god-tier Prompts and real Source Code examples!