Sandbox Mode Data Handling
⚠️ CRITICAL RULE: NEVER CALL SUPABASE FROM SANDBOX MODE
Sandbox mode stores ALL data locally using LocalDbHelper. It must NEVER make database calls to Supabase.
How to Detect Sandbox Mode
✅ CORRECT: Check the URL route
final isSandbox = SandboxService.isSandboxRoute('/app/sandbox/lists'); // true
❌ WRONG: Checking Supabase to detect sandbox
// NEVER DO THIS - THIS IS STILL CALLING SUPABASE!
final familyCheck = await _supabase.from('families').select('id').eq('id', familyId).maybeSingle();
final isSandbox = familyCheck == null; // WRONG!
If the route contains /app/sandbox, it's sandbox mode. No Supabase calls. Period.
How Sandbox Mode Works
Detection Pattern
Sandbox mode is detected by the URL route, NOT by checking Supabase.
If the route path starts with /app/sandbox, the user is in sandbox mode. NO SUPABASE CALLS ARE ALLOWED.
import 'package:go_router/go_router.dart';
import 'package:treehouse/shared/services/sandbox_service.dart';
// In a widget/service context where you have access to the route:
final router = GoRouter.of(context);
final currentRoute = router.routerDelegate.currentConfiguration.uri.path;
final isSandbox = SandboxService.isSandboxRoute(currentRoute);
// Or if you already have the route path:
final isSandbox = SandboxService.isSandboxRoute('/app/sandbox/lists'); // true
final isSandbox = SandboxService.isSandboxRoute('/lists'); // false
Key points:
- ✅ Route-based detection:
/app/sandbox/*= sandbox mode - ✅ No Supabase calls needed for detection
- ✅ Works in services by passing route context or checking current route
- ❌ NEVER check Supabase to detect sandbox mode
Storage Pattern
For sandbox families:
- ✅ Use
LocalDbHelperfor all CRUD operations - ✅ Store data locally (SQLite on mobile, SharedPreferences/IndexedDB on web)
- ✅ Mark records with
is_local_only: 1andneeds_sync: 0
For real families:
- ✅ Use Supabase directly
- ✅ Sync to/from server
Implementation Checklist
When implementing ANY new feature that stores data, you MUST:
- ✅ Check route for sandbox mode using
SandboxService.isSandboxRoute() - ✅ Use LocalDbHelper when
isSandbox == true - ✅ Add table/methods to LocalDbHelper if needed
- ✅ Never call Supabase when in sandbox mode
- ✅ Test in sandbox mode to ensure no database calls
Reference Implementation
Note: TaskService currently uses the OLD pattern (checking Supabase). This is WRONG and should be updated to use route-based detection.
The CORRECT pattern for services that need to detect sandbox mode:
// Option 1: If you have route context (e.g., in a widget)
final router = GoRouter.of(context);
final currentRoute = router.routerDelegate.currentConfiguration.uri.path;
final isSandbox = SandboxService.isSandboxRoute(currentRoute);
// Option 2: If you need to pass route info to a service
// Pass isSandbox as a parameter, or check route in the service if possible
if (isSandbox) {
// SANDBOX: Use LocalDbHelper - NO SUPABASE CALLS
await LocalDbHelper.insertTask(localTask);
} else {
// REAL: Use Supabase
await _supabase.from('tasks').insert(...);
}
For services without direct route access:
- Services should receive
isSandboxas a parameter, OR - Check the route at the widget/provider level and pass it down
- NEVER call Supabase to detect sandbox mode
LocalDbHelper Tables
Currently supported tables:
- ✅
tasks- Task operations - ✅
users- User/profile operations - ✅
families- Family operations - ✅
calendar_events- Calendar event operations - ✅
family_lists- List operations (added v10)
Adding New Tables to LocalDbHelper
When adding support for a new data type:
-
Add table constant:
static const String myTable = 'my_table'; -
Add to whitelist:
static const List<String> _allowedTableNames = [ // ... existing tables myTable, ]; -
Create table in
_onCreate():await db.execute(''' CREATE TABLE $myTable ( id TEXT PRIMARY KEY, // ... columns is_local_only INTEGER DEFAULT 1, needs_sync INTEGER DEFAULT 0, last_sync_at TEXT ) '''); -
Add migration in
_onUpgrade():if (oldVersion < X) { await db.execute('''CREATE TABLE IF NOT EXISTS $myTable ...'''); } -
Increment database version:
static const int _databaseVersion = X; -
Add CRUD methods:
insertMyData()getMyData()updateMyData()deleteMyData()
Service Implementation Pattern
Every service that stores data MUST follow this pattern:
Pattern 1: Service receives isSandbox parameter (RECOMMENDED)
class MyService {
final SupabaseClient _supabase = Supabase.instance.client;
Future<MyData> createMyData(MyData data, {required bool isSandbox}) async {
if (isSandbox) {
// SANDBOX: Use LocalDbHelper - NO SUPABASE CALLS
if (kDebugMode) {
print('🏖️ Creating in sandbox mode (local storage)');
}
await LocalDbHelper.insertMyData(data);
return data;
} else {
// REAL: Use Supabase
final response = await _supabase
.from('my_table')
.insert(data.toJson())
.select()
.single();
return MyData.fromJson(response);
}
}
Future<List<MyData>> getMyData(String familyId, {required bool isSandbox}) async {
if (isSandbox) {
// SANDBOX: Use LocalDbHelper - NO SUPABASE CALLS
return await LocalDbHelper.getMyData(familyId);
} else {
// REAL: Use Supabase
final response = await _supabase
.from('my_table')
.select()
.eq('family_id', familyId);
return (response as List)
.map((item) => MyData.fromJson(item))
.toList();
}
}
}
// Usage in widget/provider:
final router = GoRouter.of(context);
final currentRoute = router.routerDelegate.currentConfiguration.uri.path;
final isSandbox = SandboxService.isSandboxRoute(currentRoute);
await myService.createMyData(data, isSandbox: isSandbox);
Pattern 2: Check route in widget/provider, pass to service
// In widget/provider layer:
final router = GoRouter.of(context);
final currentRoute = router.routerDelegate.currentConfiguration.uri.path;
final isSandbox = SandboxService.isSandboxRoute(currentRoute);
// Call service with sandbox flag
if (isSandbox) {
await LocalDbHelper.insertMyData(data);
} else {
await myService.createMyData(data); // Service calls Supabase
}
Key rule: The route check happens at the UI/provider layer, NOT in the service by calling Supabase.
Common Mistakes to Avoid
❌ DON'T: Call Supabase without checking sandbox status
// WRONG - will fail in sandbox mode!
await _supabase.from('lists').insert(list.toJson());
❌ DON'T: Assume all families are real
// WRONG - sandbox families don't exist in Supabase!
final family = await _supabase.from('families').select().eq('id', familyId).single();
✅ DO: Always check route for sandbox mode
// CORRECT - check route, NO SUPABASE CALLS
final router = GoRouter.of(context);
final currentRoute = router.routerDelegate.currentConfiguration.uri.path;
final isSandbox = SandboxService.isSandboxRoute(currentRoute);
if (isSandbox) {
// Use LocalDbHelper - NO SUPABASE CALLS
await LocalDbHelper.insertMyData(data);
} else {
// Use Supabase
await _supabase.from('my_table').insert(data.toJson());
}
Testing
Always test new features in sandbox mode:
- Navigate to
/app/sandbox/*routes - Verify no Supabase calls are made (check network tab)
- Verify data persists locally
- Verify data appears in UI after refresh
Related Files
lib/shared/services/local_db_helper.dart- Local storage implementationlib/shared/services/sandbox_service.dart- Sandbox utilities (route detection)lib/shared/services/task_service.dart- NEEDS FIX - currently uses wrong pattern (checks Supabase)lib/shared/services/list_service.dart- NEEDS FIX - currently calls Supabase without sandbox check
Migration Notes
When migrating sandbox data to a real account:
- See
lib/shared/services/sandbox_migration_service.dart - Sandbox data is read from LocalDbHelper
- Migrated data is written to Supabase via service methods
Remember: If you're adding a new feature that stores data, you MUST handle sandbox mode. There are no exceptions.