Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .github/workflows/db-keepalive.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: db-keepalive

# SQLite Cloud parks a free node after a stretch of inactivity, and a parked
# node refuses new connections — so no amount of visitor traffic revives it,
# only a restart from https://dashboard.sqlitecloud.io. Prevention is the only
# automatable half: this keeps a query flowing so the node never goes idle
# long enough to be parked.
#
# Two caveats worth knowing before trusting it as the sole guard:
# * GitHub delays scheduled runs under load and drops them entirely when the
# repo has had no activity for 60 days, so treat the cadence as best-effort.
# * It probes through the deployed site, so a Railway outage shows up here as
# a failure too. That is deliberate — both mean c0upons is down.
on:
schedule:
- cron: '*/15 * * * *'
workflow_dispatch:

permissions:
contents: read

concurrency:
group: db-keepalive
cancel-in-progress: true

jobs:
ping:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Query the database
env:
BASE_URL: ${{ vars.KEEPALIVE_BASE_URL || 'https://c0upons.com' }}
run: |
set -uo pipefail
# Retry a couple of times so one dropped request doesn't page anyone;
# a genuinely paused node fails all three the same way.
for attempt in 1 2 3; do
status=$(curl -fsS -m 30 -o body.txt -w '%{http_code}' \
"$BASE_URL/api/health/db" || echo 000)
if [ "$status" = "200" ]; then
echo "database awake (attempt $attempt)"
exit 0
fi
echo "attempt $attempt: HTTP $status"
cat body.txt || true
sleep 10
done
echo "::error::Database unreachable after 3 attempts (last status $status)."
echo "::error::If the body says the node is paused, restart it at https://dashboard.sqlitecloud.io"
exit 1
29 changes: 29 additions & 0 deletions apps/web/app/api/health/db/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { NextResponse } from 'next/server';
import { getDb } from '@/lib/db';
import { dbErrorResponse } from '@/lib/api-error';

// Never cache: a cached 200 would keep reporting health after the node parks,
// and the keep-alive query has to actually reach SQLite Cloud to count as
// activity.
export const dynamic = 'force-dynamic';

/**
* Liveness probe for the database, and the query the keep-alive schedule runs.
*
* SQLite Cloud parks a free node after a stretch with no queries, and a parked
* node cannot be woken by traffic — only by a restart from the dashboard. So
* the cheapest cure is to never go idle: `.github/workflows/db-keepalive.yml`
* calls this on a schedule, and the `SELECT 1` is the activity that keeps the
* node awake. It doubles as monitoring — a paused node answers 503 here loudly
* instead of silently emptying the pages that swallow their own DB errors.
*/
export async function GET() {
try {
const db = getDb();
await db.sql`SELECT 1`;
return NextResponse.json({ ok: true }, { headers: { 'Cache-Control': 'no-store' } });
} catch (err) {
console.error(err);
return dbErrorResponse(err, 'Database health check failed');
}
}
Loading