Bỏ qua

Cron: Supabase vs Cloudflare

Tổng quan so sánh

┌─────────────────────────────────────────────────────────────────┐
│                      CRON COMPARISON                             │
├────────────────────────────┬────────────────────────────────────┤
│    SUPABASE (pg_cron)      │     CLOUDFLARE CRON TRIGGERS       │
├────────────────────────────┼────────────────────────────────────┤
│  PostgreSQL extension      │  Workers scheduled execution       │
│  Runs SQL/Functions        │  Runs JavaScript/TypeScript        │
│  Direct DB access          │  API access to DB                  │
│  UTC timezone only         │  UTC timezone only                 │
│  Included in plan          │  Included in Workers plan          │
└────────────────────────────┴────────────────────────────────────┘

So sánh chi tiết

Tiêu chí Supabase Cron (pg_cron) Cloudflare Cron Triggers
Chạy ở đâu Trong PostgreSQL Edge Workers (global)
Ngôn ngữ SQL / PL/pgSQL JavaScript / TypeScript
Min interval 1 phút 1 phút
Max interval Unlimited Unlimited
Timezone UTC only UTC only
Timeout Statement timeout (DB) 30s (free), 15min (paid)
DB Access Direct (same process) Via API (network call)
External APIs Via http extension Native fetch
Monitoring job_run_details table Workers Analytics
Retry Manual Manual

Supabase Cron (pg_cron)

Ưu điểm

✅ Direct Database Access
   - Không cần network call
   - Fastest for DB operations

✅ Transactional
   - ACID compliant
   - Rollback on failure

✅ Simple Setup
   - Just SQL
   - No deployment needed

✅ Data Locality
   - Same region as database
   - No latency

Nhược điểm

❌ SQL Only
   - Limited to SQL/PL/pgSQL
   - Complex logic = complex functions

❌ External APIs Difficult
   - Needs http extension
   - Not as flexible as JavaScript

❌ No Global Distribution
   - Single region
   - Latency for global operations

❌ Monitoring Limited
   - Basic logs in table
   - No built-in alerting

Cloudflare Cron Triggers

Ưu điểm

✅ Full JavaScript/TypeScript
   - Any npm package
   - Complex logic easy

✅ External API Native
   - fetch built-in
   - Webhooks, notifications easy

✅ Global Execution
   - Runs at edge
   - Low latency worldwide

✅ Rich Ecosystem
   - KV, R2, Queue integration
   - Full Workers capabilities

Nhược điểm

❌ Network Call to DB
   - Must call Supabase API
   - Extra latency

❌ Timeout Limits
   - 30s free tier
   - 15min paid tier

❌ Not Transactional with DB
   - Separate operations
   - Potential inconsistency

❌ Deployment Required
   - Code changes need deploy
   - Not as instant as SQL

Use Case: Database Maintenance

-- Cleanup expired sessions
SELECT cron.schedule(
  'cleanup-sessions',
  '0 3 * * *',  -- 3 AM daily
  $$
    DELETE FROM auth.sessions
    WHERE expires_at < NOW() - INTERVAL '7 days';

    -- Also vacuum the table
    VACUUM ANALYZE auth.sessions;
  $$
);

-- Archive old data
SELECT cron.schedule(
  'archive-old-orders',
  '0 2 * * 0',  -- 2 AM every Sunday
  $$
    WITH moved AS (
      DELETE FROM orders
      WHERE created_at < NOW() - INTERVAL '1 year'
      RETURNING *
    )
    INSERT INTO orders_archive SELECT * FROM moved;
  $$
);
// ❌ Unnecessary network calls
export default {
  async scheduled(event: ScheduledEvent, env: Env) {
    // Have to call API for each operation
    await fetch(`${env.SUPABASE_URL}/rest/v1/rpc/cleanup_sessions`, {
      method: 'POST',
      headers: { 'Authorization': `Bearer ${env.SERVICE_KEY}` },
    });
    // Slower, more complex
  }
};

Use Case: External API Calls

// Send daily digest email
export default {
  async scheduled(event: ScheduledEvent, env: Env) {
    // Get users needing digest
    const { data: users } = await supabase
      .from('users')
      .select('id, email')
      .eq('digest_enabled', true);

    // Send emails via external service
    for (const user of users) {
      await fetch('https://api.sendgrid.com/v3/mail/send', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${env.SENDGRID_KEY}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          to: user.email,
          subject: 'Daily Digest',
          // ...
        }),
      });
    }
  }
};

Supabase (More complex)

-- Needs http extension
CREATE EXTENSION IF NOT EXISTS http;

CREATE OR REPLACE FUNCTION send_daily_digest()
RETURNS void AS $$
DECLARE
  user_record RECORD;
  response http_response;
BEGIN
  FOR user_record IN
    SELECT id, email FROM users WHERE digest_enabled
  LOOP
    -- Cumbersome HTTP call from SQL
    SELECT * INTO response FROM http_post(
      'https://api.sendgrid.com/v3/mail/send',
      json_build_object('to', user_record.email)::text,
      'application/json'
    );
  END LOOP;
END;
$$ LANGUAGE plpgsql;

SELECT cron.schedule('daily-digest', '0 9 * * *', 'SELECT send_daily_digest()');

Use Case: Data Aggregation

-- Calculate daily statistics
SELECT cron.schedule(
  'daily-stats',
  '0 0 * * *',  -- Midnight
  $$
    INSERT INTO daily_stats (
      date,
      total_orders,
      total_revenue,
      new_users,
      active_users
    )
    SELECT
      CURRENT_DATE - 1 AS date,
      COUNT(*) FILTER (WHERE type = 'order') AS total_orders,
      SUM(amount) FILTER (WHERE type = 'order') AS total_revenue,
      COUNT(*) FILTER (WHERE type = 'signup') AS new_users,
      COUNT(DISTINCT user_id) AS active_users
    FROM events
    WHERE created_at::date = CURRENT_DATE - 1;
  $$
);

Cloudflare (Less efficient)

// ❌ Multiple API calls, slower
export default {
  async scheduled(event: ScheduledEvent, env: Env) {
    const yesterday = new Date();
    yesterday.setDate(yesterday.getDate() - 1);

    // Multiple queries needed
    const [orders, users, events] = await Promise.all([
      supabase.from('orders').select('*').gte('created_at', yesterday),
      supabase.from('users').select('*').gte('created_at', yesterday),
      supabase.from('events').select('*').gte('created_at', yesterday),
    ]);

    // Calculate in JS (less efficient than SQL)
    const stats = {
      total_orders: orders.data.length,
      // ...
    };

    await supabase.from('daily_stats').insert(stats);
  }
};

Use Case: Queue + Cron Hybrid

Cloudflare Cron → Supabase Queue

// Cloudflare Cron: Trigger batch job
export default {
  async scheduled(event: ScheduledEvent, env: Env) {
    // Get pending tasks
    const { data: tasks } = await supabase
      .from('scheduled_tasks')
      .select('*')
      .eq('status', 'pending')
      .lte('run_at', new Date().toISOString());

    // Enqueue to Supabase pgmq for processing
    for (const task of tasks) {
      await supabase.rpc('pgmq_send', {
        queue_name: 'task_queue',
        message: task,
      });
    }
  }
};

Supabase Cron → Cloudflare Queue

-- pg_cron: Enqueue to Cloudflare via Edge Function
CREATE OR REPLACE FUNCTION trigger_cf_processing()
RETURNS void AS $$
BEGIN
  PERFORM http_post(
    'https://xxx.workers.dev/enqueue',
    (SELECT json_agg(t) FROM pending_tasks t),
    'application/json'
  );
END;
$$ LANGUAGE plpgsql;

SELECT cron.schedule(
  'trigger-cf-queue',
  '*/5 * * * *',  -- Every 5 minutes
  'SELECT trigger_cf_processing()'
);

Decision Matrix

Chọn Supabase pg_cron khi:

┌─────────────────────────────────────────────────────────────┐
│  ✅ Database maintenance (cleanup, vacuum, archive)         │
│  ✅ Data aggregation (reports, statistics)                  │
│  ✅ Simple recurring DB updates                             │
│  ✅ Transactional operations                                │
│  ✅ No external API calls needed                            │
│  ✅ SQL logic is sufficient                                 │
└─────────────────────────────────────────────────────────────┘

Chọn Cloudflare Cron khi:

┌─────────────────────────────────────────────────────────────┐
│  ✅ External API calls (email, webhooks, notifications)    │
│  ✅ Complex business logic (easier in JS/TS)               │
│  ✅ Global execution needed                                 │
│  ✅ Integration với Workers ecosystem (KV, Queue, R2)       │
│  ✅ Long-running tasks (with paid tier)                     │
│  ✅ Multiple services orchestration                         │
└─────────────────────────────────────────────────────────────┘

Comparison Summary

┌─────────────────────────────────────────────────────────────┐
│                    DECISION FLOWCHART                        │
│                                                              │
│  Task requires direct DB operations?                        │
│         │                                                    │
│    YES ─┴─ NO                                               │
│    │       │                                                │
│    ▼       ▼                                                │
│ pg_cron   Task needs external API?                          │
│                │                                            │
│           YES ─┴─ NO                                        │
│           │       │                                         │
│           ▼       ▼                                         │
│       CF Cron   Complex JS logic?                           │
│                      │                                      │
│                 YES ─┴─ NO                                  │
│                 │       │                                   │
│                 ▼       ▼                                   │
│             CF Cron   Either works                          │
│                                                              │
└─────────────────────────────────────────────────────────────┘

Tổng kết

Quick Reference

Scenario Recommendation
Delete old records pg_cron
Generate reports from DB pg_cron
Send emails/notifications Cloudflare
Call external webhooks Cloudflare
Sync with external systems Cloudflare
Update statistics pg_cron
Complex business rules Cloudflare

Key Takeaways

  1. pg_cron = Database-centric, SQL-based, direct access
  2. Cloudflare = API-centric, JS-based, external integrations
  3. Both can work together in hybrid patterns

Q&A

  1. Jobs nào trong dự án cần external API?
  2. Có complex logic nào khó viết bằng SQL?
  3. Cần global execution không?