Skip to content

Commit 9e6f362

Browse files
committed
feat(api): add keep-alive endpoints for Supabase and Vercel cold-start mitigation
1 parent b71602b commit 9e6f362

1 file changed

Lines changed: 51 additions & 20 deletions

File tree

api/main.py

Lines changed: 51 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,27 @@
55
import time
66
import structlog
77
from contextlib import asynccontextmanager
8-
from fastapi import FastAPI, Request
8+
from fastapi import FastAPI, Request, Response
99
from fastapi.middleware.cors import CORSMiddleware
1010
from fastapi.responses import JSONResponse
1111
from slowapi import Limiter
1212
from slowapi.errors import RateLimitExceeded
1313
from slowapi.middleware import SlowAPIMiddleware
1414
from slowapi.util import get_remote_address
15-
from routers import (
16-
pinned,
17-
query,
18-
export,
19-
history,
20-
webhooks,
21-
payments,
22-
messages,
23-
analytics,
24-
legal,
25-
seo,
26-
emails,
27-
shares,
28-
)
15+
from routers import (
16+
pinned,
17+
query,
18+
export,
19+
history,
20+
webhooks,
21+
payments,
22+
messages,
23+
analytics,
24+
legal,
25+
seo,
26+
emails,
27+
shares,
28+
)
2929
from auth import get_supabase_admin
3030
from services.cache import close_redis, get_redis
3131
from services.inference import close_client
@@ -334,11 +334,11 @@ async def llm_error_handler(request: Request, exc: LLMError):
334334
app.include_router(history.router, prefix="/api")
335335
app.include_router(analytics.router, prefix="/api")
336336
app.include_router(legal.router, prefix="/api")
337-
app.include_router(seo.router)
338-
app.include_router(emails.router, prefix="/api")
339-
app.include_router(webhooks.router) # No prefix - webhooks use full path
340-
app.include_router(payments.router, prefix="/api")
341-
app.include_router(shares.router, prefix="/api")
337+
app.include_router(seo.router)
338+
app.include_router(emails.router, prefix="/api")
339+
app.include_router(webhooks.router) # No prefix - webhooks use full path
340+
app.include_router(payments.router, prefix="/api")
341+
app.include_router(shares.router, prefix="/api")
342342

343343

344344
@app.get("/api/health", tags=["health"])
@@ -378,6 +378,7 @@ async def check_rate_limit() -> dict[str, str]:
378378
log_fn("rate_limit_health_probe_failed", severity="error" if is_prod else "warning", error=str(exc))
379379
return {"status": status}
380380

381+
381382
async def check_db() -> dict[str, str]:
382383
try:
383384
if not settings.supabase_url or not settings.supabase_secret_key:
@@ -416,6 +417,36 @@ async def check_db() -> dict[str, str]:
416417
}
417418

418419

420+
@app.get("/api/keep-alive", tags=["health"])
421+
async def keep_alive(request: Request):
422+
"""Minimal keep-alive endpoint for Supabase and Vercel cold-start mitigation."""
423+
cron_secret = os.getenv("CRON_SECRET")
424+
if cron_secret:
425+
provided = request.query_params.get("key")
426+
if not provided or provided != cron_secret:
427+
return JSONResponse(status_code=401, content={"error": "unauthorized"})
428+
429+
supabase = get_supabase_admin()
430+
if not supabase:
431+
return JSONResponse(status_code=503, content={"error": "supabase_unavailable"})
432+
433+
await asyncio.to_thread(
434+
lambda: supabase.table("conversations").select("id").limit(1).execute()
435+
)
436+
return JSONResponse(status_code=200, content={"ok": True})
437+
438+
439+
@app.head("/api/keep-alive", tags=["health"])
440+
async def keep_alive_head(request: Request):
441+
"""HEAD variant that avoids Supabase calls."""
442+
cron_secret = os.getenv("CRON_SECRET")
443+
if cron_secret:
444+
provided = request.query_params.get("key")
445+
if not provided or provided != cron_secret:
446+
return Response(status_code=401)
447+
return Response(status_code=200)
448+
449+
419450
# Catch-all route for debugging (should be last)
420451
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
421452
async def catch_all(request: Request, path: str):

0 commit comments

Comments
 (0)