Treehouse
info

SANDBOX_DATA_HANDLING

Uncategorized

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 LocalDbHelper for all CRUD operations
  • ✅ Store data locally (SQLite on mobile, SharedPreferences/IndexedDB on web)
  • ✅ Mark records with is_local_only: 1 and needs_sync: 0

For real families:

  • ✅ Use Supabase directly
  • ✅ Sync to/from server

Implementation Checklist

When implementing ANY new feature that stores data, you MUST:

  1. Check route for sandbox mode using SandboxService.isSandboxRoute()
  2. Use LocalDbHelper when isSandbox == true
  3. Add table/methods to LocalDbHelper if needed
  4. Never call Supabase when in sandbox mode
  5. 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 isSandbox as 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:

  1. Add table constant:

    static const String myTable = 'my_table';
    
  2. Add to whitelist:

    static const List<String> _allowedTableNames = [
      // ... existing tables
      myTable,
    ];
    
  3. 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
      )
    ''');
    
  4. Add migration in _onUpgrade():

    if (oldVersion < X) {
      await db.execute('''CREATE TABLE IF NOT EXISTS $myTable ...''');
    }
    
  5. Increment database version:

    static const int _databaseVersion = X;
    
  6. 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:

  1. Navigate to /app/sandbox/* routes
  2. Verify no Supabase calls are made (check network tab)
  3. Verify data persists locally
  4. Verify data appears in UI after refresh

Related Files

  • lib/shared/services/local_db_helper.dart - Local storage implementation
  • lib/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.