-
Notifications
You must be signed in to change notification settings - Fork 126
Fix: Implement robust database connection with Supabase fallback #227
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Fix: Implement robust database connection with Supabase fallback #227
Conversation
- Add IPv6 connectivity detection and graceful fallback - Implement Supabase REST API as backup for PostgreSQL failures - Add retry mechanism with exponential backoff - Improve error handling and user messaging - Fix missing trending_niches table error in AI routes - Add comprehensive database connection diagnostics Resolves database connection issues and ensures server stability even when direct PostgreSQL connection fails.
WalkthroughThe backend adds resilient connection testing, database diagnostics, and Supabase fallback mechanisms to the PostgreSQL initialization flow. Changes introduce connection retry logic, conditional seeding with existence checks, and fallback APIs for trending niches when PostgreSQL is unavailable. A new Supabase service layer manages client lifecycle and data operations. Changes
Sequence Diagram(s)sequenceDiagram
participant App as Application Startup
participant PG as PostgreSQL
participant DB as Database Service
participant SUP as Supabase Service
participant FS as Fallback System
App->>PG: test_connection_with_retry()
alt PostgreSQL Available
PG-->>DB: Connection Success
DB->>DB: create_tables()
DB->>DB: seed_db()
Note over App: PostgreSQL mode active
else PostgreSQL Unavailable
PG-->>DB: Connection Failed
DB->>SUP: connect()
alt Supabase Available
SUP-->>DB: Connection Success
SUP->>SUP: create_tables()
SUP->>SUP: seed_data()
Note over App: Supabase fallback mode active
else Both Unavailable
SUP-->>DB: Connection Failed
Note over App: Limited mode<br/>(Hardcoded fallbacks only)
end
end
Note over App: Startup complete
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~30 minutes
Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🧹 Nitpick comments (9)
Backend/app/db/seed.py (2)
8-29: Remove unusedpasswordfield from seed data.The
Usermodel (per the relevant snippet inmodels.py) does not have apasswordorpassword_hashcolumn—authentication is handled by Supabase. Includingpasswordin the seed dictionaries is misleading and could cause confusion for future maintainers.{ "id": "aabb1fd8-ba93-4e8c-976e-35e5c40b809c", "username": "creator1", "email": "[email protected]", - "password": "password123", "role": "creator", "bio": "Lifestyle and travel content creator", "profile_image": None, "created_at": datetime.utcnow() }, { "id": "6dbfcdd5-795f-49c1-8f7a-a5538b8c6f6f", "username": "brand1", "email": "[email protected]", - "password": "password123", "role": "brand", "bio": "Sustainable fashion brand looking for influencers", "profile_image": None, "created_at": datetime.utcnow() },
35-38: Consider using SQLAlchemy 2.0 style select.Using
User.__table__.select()is a lower-level pattern. The idiomatic SQLAlchemy 2.0 approach isselect(User).where(...), which provides better type hints and ORM integration.+from sqlalchemy import select + # Check if user exists -existing_user = await session.execute( - User.__table__.select().where(User.email == user_data["email"]) -) -existing_user = existing_user.scalar_one_or_none() +result = await session.execute( + select(User).where(User.email == user_data["email"]) +) +existing_user = result.scalar_one_or_none()Backend/app/services/supabase_service.py (2)
1-6: Remove unusedasyncioimport.The
asynciomodule is imported but never used in this file.import os -import asyncio from typing import Optional, Dict, Any, List from supabase import create_client, Client from dotenv import load_dotenv
35-68: Method name is misleading - it checks but doesn't create tables.
create_tables()returnsTrueeven when tables don't exist (line 64), which could mislead callers into thinking tables were created. Consider renaming tocheck_tables()orverify_tables(), or returningFalsewhen tables need manual creation.- async def create_tables(self) -> bool: - """Check if tables exist or print instructions for manual creation""" + async def verify_tables(self) -> bool: + """Check if required tables exist, print instructions if missing""" ... print("✅ Supabase client is ready - tables need manual creation") - return True + return False # Tables don't existBackend/app/main.py (1)
87-92: Unnecessary try/except in home endpoint.The
home()endpoint only returns a static dictionary, which cannot raise an exception. The try/except wrapper adds unnecessary complexity.@app.get("/") async def home(): - try: - return {"message": "Welcome to Inpact API!"} - except Exception as e: - return {"error": f"Unexpected error: {e}"} + return {"message": "Welcome to Inpact API!"}Backend/app/routes/ai.py (2)
16-18: Redundant and fragile environment loading.Environment variables are already loaded in
main.pyanddb.pyviaload_dotenv(). This custom path-based loading is redundant and fragile (breaks if file moves). Remove this and rely on the centralized loading.-# Load environment variables from Backend/.env -env_path = Path(__file__).parent.parent.parent / '.env' -load_dotenv(dotenv_path=env_path) +# Environment variables are loaded in main.py
153-161: Consider batch insert instead of loop.Inserting records one at a time creates 6 separate API calls. The Supabase client supports batch insert, which is more efficient.
- for niche in niches: - supabase.table("trending_niches").insert({ - "name": niche["name"], - "insight": niche["insight"], - "global_activity": int(niche["global_activity"]), - "fetched_at": today - }).execute() + records = [{ + "name": niche["name"], + "insight": niche["insight"], + "global_activity": int(niche["global_activity"]), + "fetched_at": today + } for niche in niches] + supabase.table("trending_niches").insert(records).execute()Backend/app/db/db.py (2)
83-83: Remove extraneousfprefix - no placeholders in string.The f-string on line 83 has no placeholders, making the
fprefix unnecessary.- return False, f"IPv6 connectivity issue: Your system cannot resolve the IPv6-only Supabase database host. Using REST API fallback." + return False, "IPv6 connectivity issue: Your system cannot resolve the IPv6-only Supabase database host. Using REST API fallback."
96-96: Remove extraneousfprefix.Same issue - this string has no placeholders.
- print(f"🔄 Retrying in 2 seconds...") + print("🔄 Retrying in 2 seconds...")
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
Backend/app/db/db.py(1 hunks)Backend/app/db/seed.py(1 hunks)Backend/app/main.py(2 hunks)Backend/app/routes/ai.py(3 hunks)Backend/app/services/supabase_service.py(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
Backend/app/services/supabase_service.py (2)
Frontend/src/utils/supabase.tsx (1)
supabase(11-11)Backend/app/main.py (1)
create_tables(22-30)
Backend/app/main.py (3)
Backend/app/db/db.py (1)
test_connection_with_retry(87-101)Backend/app/db/seed.py (1)
seed_db(6-61)Backend/app/services/supabase_service.py (3)
create_tables(35-68)connect(16-33)seed_data(70-105)
Backend/app/db/seed.py (1)
Backend/app/models/models.py (1)
User(25-53)
🪛 Ruff (0.14.8)
Backend/app/services/supabase_service.py
29-29: Consider moving this statement to an else block
(TRY300)
31-31: Do not catch blind exception: Exception
(BLE001)
43-43: Local variable result is assigned to but never used
Remove assignment to unused variable result
(F841)
45-45: Consider moving this statement to an else block
(TRY300)
46-46: Do not catch blind exception: Exception
(BLE001)
66-66: Do not catch blind exception: Exception
(BLE001)
99-99: Local variable result is assigned to but never used
Remove assignment to unused variable result
(F841)
101-101: Consider moving this statement to an else block
(TRY300)
103-103: Do not catch blind exception: Exception
(BLE001)
Backend/app/db/db.py
76-76: Consider moving this statement to an else block
(TRY300)
80-80: Do not catch blind exception: Exception
(BLE001)
83-83: f-string without any placeholders
Remove extraneous f prefix
(F541)
96-96: f-string without any placeholders
Remove extraneous f prefix
(F541)
106-106: Create your own exception
(TRY002)
106-106: Avoid specifying long messages outside the exception class
(TRY003)
Backend/app/routes/ai.py
27-27: Avoid specifying long messages outside the exception class
(TRY003)
71-71: Consider moving this statement to an else block
(TRY300)
72-72: Do not catch blind exception: Exception
(BLE001)
151-151: Abstract raise to an inner function
(TRY301)
151-151: Create your own exception
(TRY002)
151-151: Avoid specifying long messages outside the exception class
(TRY003)
162-162: Do not catch blind exception: Exception
(BLE001)
168-168: Abstract raise to an inner function
(TRY301)
168-168: Create your own exception
(TRY002)
168-168: Avoid specifying long messages outside the exception class
(TRY003)
169-169: Do not catch blind exception: Exception
(BLE001)
173-173: Consider moving this statement to an else block
(TRY300)
175-175: Do not catch blind exception: Exception
(BLE001)
Backend/app/main.py
28-28: Do not catch blind exception: Exception
(BLE001)
35-35: Unused function argument: app
(ARG001)
48-48: Do not catch blind exception: Exception
(BLE001)
51-51: String contains ambiguous ℹ (INFORMATION SOURCE). Did you mean i (LATIN SMALL LETTER I)?
(RUF001)
Backend/app/db/seed.py
59-59: Do not catch blind exception: Exception
(BLE001)
🔇 Additional comments (5)
Backend/app/db/seed.py (1)
59-61: LGTM - graceful error handling aligns with PR goals.The broad exception handling here is intentional to ensure the server continues startup even if seeding fails. This matches the PR objective of zero downtime.
Backend/app/routes/ai.py (1)
127-177: LGTM - robust multi-layer fallback logic.The fallback chain (table check → today's data → Gemini fetch → recent DB data → hardcoded fallback) provides excellent resilience. While the nesting is deep, the behavior correctly ensures data is always returned.
Backend/app/db/db.py (3)
63-69: IPv6 error detection is Windows-specific.The error code
11001(WSAHOST_NOT_FOUND) is Windows-specific. On Linux/macOS, DNS resolution failures use different error codes (e.g.,EAI_NONAME = -2). This check may not detect IPv6 issues on non-Windows systems.Consider checking the error type or message more broadly:
except socket.gaierror as dns_error: - if "11001" in str(dns_error): + error_str = str(dns_error).lower() + if "11001" in error_str or "name or service not known" in error_str or dns_error.errno in (-2, -5, 11001): return False, f"IPv6 connectivity issue detected for {HOST}. Your network may not support IPv6, but Supabase REST API is available as fallback." else: return False, f"DNS resolution failed: {dns_error}"
104-116: LGTM - proper session lifecycle management.The session handling with commit/rollback/close pattern is correct. The guard check on line 105-106 ensures clear failure when the database is not configured.
24-38: LGTM - good engine configuration for Supabase.The configuration with
NullPool, SSL requirement, command timeout, and application name is appropriate for connecting to Supabase PostgreSQL.NullPoolis suitable for async contexts where connection pooling is managed differently.
| # Try Supabase REST API as fallback | ||
| supabase_connected = await supabase_service.connect() | ||
| if supabase_connected: | ||
| print("✅ Using Supabase REST API for database operations") | ||
| await supabase_service.create_tables() | ||
| await supabase_service.seed_data() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Handle potential errors from Supabase fallback operations.
If supabase_service.create_tables() or seed_data() fail, the exceptions are not caught, potentially crashing the startup. Consider wrapping these in a try/except for consistency with the PostgreSQL path.
supabase_connected = await supabase_service.connect()
if supabase_connected:
print("✅ Using Supabase REST API for database operations")
- await supabase_service.create_tables()
- await supabase_service.seed_data()
+ try:
+ await supabase_service.create_tables()
+ await supabase_service.seed_data()
+ except Exception as e:
+ print(f"⚠️ Supabase initialization error: {e}")
else:
print("🚀 Server starting in limited mode without database...")📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # Try Supabase REST API as fallback | |
| supabase_connected = await supabase_service.connect() | |
| if supabase_connected: | |
| print("✅ Using Supabase REST API for database operations") | |
| await supabase_service.create_tables() | |
| await supabase_service.seed_data() | |
| # Try Supabase REST API as fallback | |
| supabase_connected = await supabase_service.connect() | |
| if supabase_connected: | |
| print("✅ Using Supabase REST API for database operations") | |
| try: | |
| await supabase_service.create_tables() | |
| await supabase_service.seed_data() | |
| except Exception as e: | |
| print(f"⚠️ Supabase initialization error: {e}") |
🤖 Prompt for AI Agents
In Backend/app/main.py around lines 54 to 59, the supabase fallback calls to
supabase_service.create_tables() and supabase_service.seed_data() are unguarded
and can raise exceptions; wrap these calls in a try/except block that catches
Exception, logs the error with context (including exception details), and
handles failure the same way as the PostgreSQL path (e.g., exit the process or
return a non-success status) to prevent an unhandled crash during startup.
| async def connect(self) -> bool: | ||
| """Initialize Supabase client""" | ||
| try: | ||
| if not self.url or not self.key: | ||
| print("❌ Supabase URL or Key not configured") | ||
| return False | ||
|
|
||
| self.client = create_client(self.url, self.key) | ||
|
|
||
| # Test connection with a simple health check | ||
| # Just initialize the client - actual table checks will happen during table creation | ||
| self._connected = True | ||
| print("✅ Supabase REST API client initialized!") | ||
| return True | ||
|
|
||
| except Exception as e: | ||
| print(f"❌ Supabase connection failed: {e}") | ||
| return False |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
supabase-py async support 2024 2025
💡 Result:
Short answer: By 2024–2025 the Supabase Python ecosystem moved toward async-first components (separate async libraries) while the central supabase PyPI package continued work to add/complete async support across the whole SDK.
Key points and sources:
- Supabase split functionality into smaller Python packages that expose Async clients (postgrest-py, realtime-py, auth-py, storage-py, functions-py) — each provides Async* client usage examples. [1][2][3][4]
- The supabase PyPI package roadmap explicitly lists “Add async support across the entire library” and wrapping the async subpackages as work in progress. [5][6]
- Community/maintainers discussion exists about unifying sync/async clients (issue tracking ongoing in 2024–2025). [7]
- There are also third‑party / community packages (e.g., supabase-py-async) providing async-style wrappers; check security/version details before use. [8]
If you want, I can:
- show example code for using AsyncPostgrestClient / AsyncRealtimeClient / AsyncGoTrueClient, or
- check the exact supabase package release notes for a specific date/version.
Sources:
- postgrest-py Async usage docs. [1]
- realtime-py Async client README. [2]
- auth-py (GoTrue port) Async examples. [3]
- functions-py AsyncFunctionsClient README. [4]
- supabase PyPI (roadmap notes mentioning async support). [5]
- supabase PyPI changelog / recent releases. [6]
- GitHub issue: Unify sync and async clients (supabase-py). [7]
- supabase-py-async package metadata (community package). [8]
(If you want links or the exact code snippets, tell me which Async client you care about and I’ll paste examples.)
🌐 Web query:
supabase-py client synchronous or asynchronous
💡 Result:
Short answer: supabase-py supports both synchronous and asynchronous usage.
- Synchronous: use create_client() / Client for normal (blocking) code. [1]
- Asynchronous: use acreate_client() / AsyncClient (and async-specific subclients like AsyncRealtime) for async/await code — realtime features require the async client. [2]
- Repo and examples confirm both clients exist in the supabase-py monorepo. [3]
Sources:
[1] Supabase Python init docs.
[2] Supabase Python realtime/async docs.
[3] supabase-py GitHub repo.
🌐 Web query:
asyncio.to_thread() blocking I/O synchronous operations
💡 Result:
Short summary
- asyncio.to_thread(func, *args, **kwargs) runs func in a separate thread (uses asyncio’s default ThreadPoolExecutor) and returns an awaitable; it propagates contextvars from the calling task. Use it to avoid blocking the event loop when calling synchronous, I/O-bound code. [1]
Key points and gotchas
- Purpose: make blocking I/O (file ops, blocking libraries, time.sleep(), blocking network clients) non-blocking to the event loop by executing them on a worker thread. It does not convert the function into an async coroutine. [1]
- GIL: because of Python’s GIL, to_thread is effective for I/O-bound work. CPU-bound pure-Python work won’t gain parallel CPU speed (use ProcessPoolExecutor for that). [1][3]
- Executor choice: to_thread always uses the default thread pool; you cannot pass a custom executor. If you need a custom ThreadPoolExecutor (size, lifetime, separate pool), use loop.run_in_executor or set_default_executor. [2][1]
- Thread-safety: the called function runs on some worker thread — ensure the objects it accesses are thread-safe (or serialize with locks). Multiple to_thread calls can run concurrently on different threads. If you need serialization, use a lock around the call. [2]
- Resource exhaustion: many simultaneous to_thread calls can consume thread-pool workers and cause scheduling delays; tune the pool or limit concurrency (e.g., a semaphore) if needed. [2]
- Alternatives: prefer native async libraries (aiofiles, aiohttp, async DB drivers) when available for better scalability; use to_thread for quick integration of blocking libraries or one-off blocking calls. [1][5]
Minimal example
import asyncio, time
def blocking_io(x):
time.sleep(1)
return x*2
async def main():
results = await asyncio.gather(
asyncio.to_thread(blocking_io, 1),
asyncio.to_thread(blocking_io, 2),
asyncio.sleep(0.5)
)
print(results)
References
- Python asyncio.to_thread docs (usage, contextvar propagation, note about GIL / I/O-bound use). [1]
- AnyIO / discussion about to_thread vs run_in_executor (default executor, no custom-executor argument, contextvar behavior). [2]
- Notes on GIL and I/O vs CPU-bound tasks (concurrency implications). [3]
Would you like an example showing how to limit concurrency (semaphore) or how to use a custom ThreadPoolExecutor with run_in_executor?
Use AsyncClient from supabase-py for proper async support instead of wrapping sync calls with asyncio.to_thread().
The Supabase Python client (supabase-py) provides both synchronous (create_client()) and asynchronous (acreate_client() / AsyncClient) APIs. Since these methods are declared async, use the async variants to avoid blocking the event loop. asyncio.to_thread() is designed for one-off blocking I/O operations, not for wrapping entire library clients. Refactor to use acreate_client(url, key) instead.
🧰 Tools
🪛 Ruff (0.14.8)
29-29: Consider moving this statement to an else block
(TRY300)
31-31: Do not catch blind exception: Exception
(BLE001)
🤖 Prompt for AI Agents
In Backend/app/services/supabase_service.py around lines 16 to 33, the async
connect method is creating a synchronous Supabase client (create_client) which
blocks the event loop; replace it with the async API by awaiting the async
client factory (use acreate_client or AsyncClient from supabase-py), set
self.client to the awaited async client instance, update type hints to
AsyncClient, remove any asyncio.to_thread usage, and ensure the module imports
the async factory/class; keep the same health-check logic and error handling but
await client creation instead of calling the sync factory.
| seed_users = [ | ||
| { | ||
| "username": "creator1", | ||
| "email": "[email protected]", | ||
| "role": "creator", | ||
| "bio": "Lifestyle and travel content creator" | ||
| }, | ||
| { | ||
| "username": "brand1", | ||
| "email": "[email protected]", | ||
| "role": "brand", | ||
| "bio": "Sustainable fashion brand looking for influencers" | ||
| } | ||
| ] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seed data differs from seed.py - potential inconsistency.
The seed users here are missing id and profile_image fields that are present in Backend/app/db/seed.py. If the application code references the hardcoded UUIDs (e.g., aabb1fd8-ba93-4e8c-976e-35e5c40b809c), the Supabase fallback path will create users with different auto-generated IDs, causing inconsistencies.
Consider aligning both seed implementations or extracting seed data to a shared constant.
🤖 Prompt for AI Agents
Backend/app/services/supabase_service.py lines 84-97: the seed_users array omits
the id and profile_image fields present in Backend/app/db/seed.py which can lead
to mismatched UUIDs when the Supabase fallback creates users; update these seed
entries to include the same id (hardcoded UUIDs) and profile_image values used
in db/seed.py, or refactor to import/consume a shared seed constant/module so
both places use identical user objects to prevent ID/profile inconsistencies.
fixes #228
🐛 Fix: Database Connection Issues with Robust Fallback System
Problem
trending_nichestable causing API errorsSolution
Changes Made
Testing
Impact
Summary by CodeRabbit
Release Notes
New Features
Bug Fixes
✏️ Tip: You can customize this high-level summary in your review settings.