From cc-mobile-flutter
Local database patterns for this Flutter project — drift with sqlcipher, schema definition, DAOs, migrations, watch queries, and repository integration. Load whenever writing or editing a table, DAO, or migration.
How this skill is triggered — by the user, by Claude, or both
Slash command
/cc-mobile-flutter:drift-persistenceThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Encrypted SQLite with a type-safe Dart query API. Schema and DAOs are colocated per feature unless a table is genuinely shared.
Encrypted SQLite with a type-safe Dart query API. Schema and DAOs are colocated per feature unless a table is genuinely shared.
// core/database/app_database.dart
@DriftDatabase(tables: [Activities, Users, Goals], daos: [ActivityDao, UserDao, GoalDao])
class AppDatabase extends _$AppDatabase {
AppDatabase(QueryExecutor executor) : super(executor);
@override
int get schemaVersion => 3;
@override
MigrationStrategy get migration => MigrationStrategy(
onCreate: (m) => m.createAll(),
onUpgrade: (m, from, to) async {
if (from < 2) await m.addColumn(activities, activities.lastSyncedAt);
if (from < 3) await m.createIndex(Index('idx_activities_user',
'CREATE INDEX idx_activities_user ON activities(user_id)'));
},
);
}
// core/database/executor.dart
QueryExecutor openConnection({required String passphrase}) {
return LazyDatabase(() async {
final dir = await getApplicationDocumentsDirectory();
final file = File(p.join(dir.path, 'app.db'));
return NativeDatabase.createInBackground(
file,
setup: (raw) => raw.execute("PRAGMA key = '$passphrase';"),
);
});
}
sqlcipher_flutter_libs provides both SQLite and encryption. Don't also include sqlite3_flutter_libs — they conflict.flutter_secure_storage. Generate on first launch with crypto.Random.secure() + base64.NativeDatabase.createInBackground to keep opens off the UI isolate.Prefer per-feature table definitions; register them with the central AppDatabase.
// feature/activity/data/local/activities_table.dart
class Activities extends Table {
TextColumn get id => text()();
TextColumn get title => text()();
TextColumn get kind => textEnum<ActivityKindRow>()();
DateTimeColumn get createdAt => dateTime()();
DateTimeColumn get lastSyncedAt => dateTime().nullable()();
@override
Set<Column> get primaryKey => {id};
}
Rules:
snake_case (drift generates them from the getter name, but you can override via .named('...')).dateTime() — drift stores ISO-8601, safe across timezones if you always write UTC (DateTime.now().toUtc()).@DataClassName annotations. Don't skip them — unindexed WHERE on millions of rows is slow even with encryption.// feature/activity/data/local/activity_dao.dart
@DriftAccessor(tables: [Activities])
class ActivityDao extends DatabaseAccessor<AppDatabase> with _$ActivityDaoMixin {
ActivityDao(super.db);
Future<ActivityRow?> getActivity(String id) =>
(select(activities)..where((t) => t.id.equals(id))).getSingleOrNull();
Stream<ActivityRow?> watchActivity(String id) =>
(select(activities)..where((t) => t.id.equals(id))).watchSingleOrNull();
Future<void> upsert(ActivityRow row) =>
into(activities).insertOnConflictUpdate(row);
Future<int> purgeStale(DateTime before) =>
(delete(activities)..where((t) => t.lastSyncedAt.isSmallerThanValue(before))).go();
}
Rules:
watch* queries are cheap and first-class. Repositories that expose Stream<Either<Failure, T>> usually drive them from watch.schemaVersion with every schema change. Never edit a past migration; always add a new step.drift_dev schema dump + drift_dev schema generate makes this straightforward — commit the schema snapshots under drift_schemas/.@override
Stream<Either<Failure, Activity>> watchActivity(String id) => _dao
.watchActivity(id)
.map((row) => row == null
? Left(const NotFoundFailure(message: 'no local record'))
: Right(row.toDomain()));
@override
Future<Either<Failure, Activity>> refreshActivity(String id) async {
final result = await handleApiCall(() => _api.getActivity(id: id), map: (d) => d.toDomain());
return result.match(
(failure) => Left(failure),
(activity) async {
await _dao.upsert(activity.toRow());
return Right(activity);
},
);
}
NetworkFailure; they're CacheFailure. Map them in the repo.NativeDatabase.memory() for unit tests — fast, isolated, no files.AppDatabase into repositories, not a DAO globally — makes fakes trivial.dynamic or Map<String, Object?>. Always row types.flutter_secure_storage exists for a reason.npx claudepluginhub dimitriremoiville/cc-mobile --plugin cc-mobile-flutterProvides UI/UX resources: 50+ styles, color palettes, font pairings, guidelines, charts for web/mobile across React, Next.js, Vue, Svelte, Tailwind, React Native, Flutter. Aids planning, building, reviewing interfaces.
Searches MemPalace before answering questions about past work, people, projects, or prior decisions. Returns verbatim stored content instead of guessing from model memory.