Supabase 保活[Cloudflare Workers部署]

1. 保活代码

/**
 * Supabase Keep-Alive Cloudflare Worker
 * 用于定期保活 Supabase 数据库连接
 */

// =============================
// 工具函数
// =============================

/**
 * 解析和验证环境变量中的 Supabase 配置
 * @param {string} configRaw - 原始配置字符串
 * @returns {Array|null} - 解析后的配置数组或 null(如果出错)
 */
function parseSupabaseConfig(configRaw) {
  try {
    const configList = JSON.parse(configRaw || '[]');
    
    if (!Array.isArray(configList)) {
      throw new Error('SUPABASE_CONFIG 必须是 JSON 数组格式');
    }
    
    if (configList.length === 0) {
      throw new Error('SUPABASE_CONFIG 不能为空数组');
    }
    
    // 校验每一项的必要字段
    const requiredFields = ['name', 'supabase_url', 'supabase_key', 'table_name'];
    configList.forEach((conf, idx) => {
      const missing = requiredFields.filter(field => !conf[field]);
      if (missing.length > 0) {
        throw new Error(`配置 index=${idx} 缺少字段: ${missing.join(', ')}`);
      }
    });
    
    return configList;
  } catch (error) {
    console.error('配置解析错误:', error.message);
    return null;
  }
}

/**
 * 对指定配置执行 keep-alive 查询
 * @param {Object} conf - Supabase 配置对象
 * @returns {Promise<{success: boolean, message: string}>}
 */
async function performPing(conf) {
  try {
    const url = `${conf.supabase_url}/rest/v1/${conf.table_name}?select=*&limit=1`;
    const headers = {
      'apikey': conf.supabase_key,
      'Authorization': `Bearer ${conf.supabase_key}`,
      'Content-Type': 'application/json'
    };
    
    const response = await fetch(url, {
      method: 'GET',
      headers: headers
    });
    
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }
    
    return { success: true, message: 'ok' };
  } catch (error) {
    return { success: false, message: error.message };
  }
}

/**
 * 根据索引获取配置
 * @param {Array} configList - 配置列表
 * @param {number} idx - 索引
 * @returns {Object} - 配置对象
 */
function getConfigByIndex(configList, idx) {
  if (idx < 0 || idx >= configList.length) {
    throw new Error(`index ${idx} 不存在`);
  }
  return configList[idx];
}

/**
 * 根据名称获取配置
 * @param {Array} configList - 配置列表
 * @param {string} name - 配置名称
 * @returns {Object} - 配置对象
 */
function getConfigByName(configList, name) {
  const conf = configList.find(c => c.name === name);
  if (!conf) {
    throw new Error(`name '${name}' 未找到对应配置`);
  }
  return conf;
}

/**
 * 创建 JSON 响应
 * @param {number} status - HTTP 状态码
 * @param {Object} body - 响应体
 * @returns {Response} - Response 对象
 */
function createJsonResponse(status, body) {
  return new Response(JSON.stringify(body), {
    status: status,
    headers: {
      'Content-Type': 'application/json',
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
      'Access-Control-Allow-Headers': 'Content-Type'
    }
  });
}

// =============================
// 路由处理函数
// =============================

/**
 * 处理所有配置的保活请求
 * @param {Array} configList - 配置列表
 * @returns {Promise<Response>}
 */
async function handleKeepAliveAll(configList) {
  let successCount = 0;
  const results = [];
  
  for (let idx = 0; idx < configList.length; idx++) {
    const conf = configList[idx];
    const result = await performPing(conf);
    results.push({
      name: conf.name,
      index: idx,
      success: result.success,
      message: result.message
    });
    
    if (result.success) {
      successCount++;
    }
  }
  
  let status, message;
  if (successCount === configList.length) {
    status = 'success';
    message = 'ok';
    return createJsonResponse(200, { status, message, results });
  } else if (successCount > 0) {
    status = 'error';
    message = 'partial_failure';
    return createJsonResponse(500, { status, message, results });
  } else {
    status = 'error';
    message = 'all_failure';
    return createJsonResponse(500, { status, message, results });
  }
}

/**
 * 处理按索引的保活请求
 * @param {Array} configList - 配置列表
 * @param {number} idx - 配置索引
 * @returns {Promise<Response>}
 */
async function handleKeepAliveByIndex(configList, idx) {
  try {
    const conf = getConfigByIndex(configList, idx);
    const result = await performPing(conf);
    const statusCode = result.success ? 200 : 500;
    const status = result.success ? 'success' : 'error';
    
    return createJsonResponse(statusCode, {
      status,
      message: result.message,
      config_name: conf.name,
      config_index: idx
    });
  } catch (error) {
    return createJsonResponse(404, {
      status: 'error',
      message: error.message
    });
  }
}

/**
 * 处理按名称的保活请求
 * @param {Array} configList - 配置列表
 * @param {string} name - 配置名称
 * @returns {Promise<Response>}
 */
async function handleKeepAliveByName(configList, name) {
  try {
    const conf = getConfigByName(configList, name);
    const result = await performPing(conf);
    const statusCode = result.success ? 200 : 500;
    const status = result.success ? 'success' : 'error';
    
    return createJsonResponse(statusCode, {
      status,
      message: result.message,
      config_name: conf.name
    });
  } catch (error) {
    return createJsonResponse(404, {
      status: 'error',
      message: error.message
    });
  }
}

// =============================
// 主要事件处理器
// =============================

export default {
  /**
   * 处理 HTTP 请求
   * @param {Request} request - 请求对象
   * @param {Object} env - 环境变量
   * @param {Object} ctx - 执行上下文
   * @returns {Promise<Response>}
   */
  async fetch(request, env, ctx) {
    // 解析配置
    const configList = parseSupabaseConfig(env.SUPABASE_CONFIG);
    if (!configList) {
      return createJsonResponse(500, {
        status: 'error',
        message: 'Startup failed: 配置解析失败,请检查 SUPABASE_CONFIG 环境变量'
      });
    }
    
    const url = new URL(request.url);
    const pathname = url.pathname;
    
    // 处理 CORS 预检请求
    if (request.method === 'OPTIONS') {
      return createJsonResponse(200, {});
    }
    
    // 路由匹配
    if (pathname === '/api/keepalive' || pathname === '/api/keepalive/all') {
      return await handleKeepAliveAll(configList);
    }
    
    // 匹配 /api/keepalive/index 或 /api/keepalive/index/{idx}
    const indexMatch = pathname.match(/^\/api\/keepalive\/index(?:\/(\d+))?$/);
    if (indexMatch) {
      const idx = indexMatch[1] ? parseInt(indexMatch[1]) : 0;
      return await handleKeepAliveByIndex(configList, idx);
    }
    
    // 匹配 /api/keepalive/name/{name}
    const nameMatch = pathname.match(/^\/api\/keepalive\/name\/(.+)$/);
    if (nameMatch) {
      const name = decodeURIComponent(nameMatch[1]);
      return await handleKeepAliveByName(configList, name);
    }
    
    // 根路径返回简单的状态信息
    if (pathname === '/') {
      return createJsonResponse(200, {
        status: 'running',
        message: 'Supabase Keep-Alive Worker',
        endpoints: [
          '/api/keepalive - 保活所有配置',
          '/api/keepalive/all - 保活所有配置',
          '/api/keepalive/index/{idx} - 按索引保活',
          '/api/keepalive/name/{name} - 按名称保活'
        ],
        total_configs: configList.length
      });
    }
    
    // 404 处理
    return createJsonResponse(404, {
      status: 'error',
      message: 'Not Found'
    });
  },

  /**
   * 处理 Cron 触发器
   * @param {Object} event - Cron 事件
   * @param {Object} env - 环境变量
   * @param {Object} ctx - 执行上下文
   */
  async scheduled(event, env, ctx) {
    console.log('Cron 触发器启动,执行定时保活任务');
    
    // 解析配置
    const configList = parseSupabaseConfig(env.SUPABASE_CONFIG);
    if (!configList) {
      console.error('配置解析失败,跳过此次执行');
      return;
    }
    
    // 执行保活任务
    let successCount = 0;
    for (let idx = 0; idx < configList.length; idx++) {
      const conf = configList[idx];
      const result = await performPing(conf);
      
      if (result.success) {
        successCount++;
        console.log(`✅ ${conf.name} (index: ${idx}) - 保活成功`);
      } else {
        console.error(`❌ ${conf.name} (index: ${idx}) - 保活失败: ${result.message}`);
      }
    }
    
    console.log(`保活任务完成: ${successCount}/${configList.length} 成功`);
  }
};

2. 配置环境变量

在 Cloudflare workers 面板中配置
变量名称: SUPABASE_CONFIG,类型: 文本,值:

[
  {
    "name": "MyPersonalProject",
    "supabase_url": "https://xyzabcjkl.supabase.co",
    "supabase_key": "key-for-project-1...",
    "table_name": "logs"
  },
  {
    "name": "AnotherApp",
    "supabase_url": "https://anotherproject.supabase.co",
    "supabase_key": "key-for-project-2...",
    "table_name": "visits"
  }
]

如果只保活一个项目,写一个即可。
supabase_url 在 Project Settings -> Data API 页面
supabase_key 在 Project Settings -> API Keys -> Legacy API Keys 页面
Table 可通过网页提供的 Table Editor 自行创建。

3. 配置 Cron

在 Workers 设置中找到触发事件

然后添加一个 Cron触发器 , 触发时间可自行定义,周期要小于一周。

4. 运行情况监控

通过 cloudflare 提供的日志,查看是否保活成功

在 Supabase Project overview 中查看是否访问成功

可以把 cron 配置为 1 分钟,用来测试,测试没问题了再改回去。