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 分钟,用来测试,测试没问题了再改回去。