Claude-skill-registry local-first
Enforces local-first architecture principles for Breath of Now. Use this skill when working with data, state management, or sync features. Ensures IndexedDB (Dexie.js) is always the source of truth.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/local-first" ~/.claude/skills/majiayu000-claude-skill-registry-local-first && rm -rf "$T"
manifest:
skills/data/local-first/SKILL.mdsource content
Local-First Architecture Skill
Este skill garante que todas as operações de dados no Breath of Now seguem o princípio local-first: dados do utilizador são armazenados localmente por defeito, com cloud sync como feature premium opcional.
Arquitectura
┌─────────────────────────────────────────────────┐ │ Browser │ │ ┌─────────────┐ ┌─────────────┐ │ │ │ IndexedDB │ │ Zustand │ │ │ │ (Dexie.js) │ │ (State) │ │ │ │ SOURCE OF │ │ UI State │ │ │ │ TRUTH │ │ Only │ │ │ └──────┬──────┘ └──────┬──────┘ │ │ │ │ │ │ └────────┬───────┘ │ │ ▼ │ │ ┌───────────────┐ │ │ │ Sync Engine │ (Premium only) │ │ │ src/lib/sync │ │ │ └───────┬───────┘ │ └─────────────────┼───────────────────────────────┘ │ (quando online + autenticado + premium) ▼ ┌───────────────┐ │ Supabase │ │ (OPCIONAL) │ └───────────────┘
Quando Usar
Aplica este skill quando:
- Criar modelos de dados ou schemas
- Implementar operações CRUD
- Construir funcionalidade de sync
- Trabalhar com preferências do utilizador
- Tratar cenários offline
Regras Fundamentais
Regra 1: IndexedDB é SEMPRE a Source of Truth
// ❌ ERRADO - Fetch directo do Supabase const { data } = await supabase.from('expenses').select('*'); setExpenses(data); // ✅ CORRECTO - Ler da BD local import { db } from '@/lib/db'; const expenses = await db.expenses.toArray(); setExpenses(expenses);
Regra 2: Escrever Localmente Primeiro, Sync Depois
// ❌ ERRADO - Escrever na cloud primeiro await supabase.from('expenses').insert(expense); // ✅ CORRECTO - Escrever localmente, queue para sync import { db } from '@/lib/db'; await db.expenses.add({ ...expense, localId: crypto.randomUUID(), syncStatus: 'pending', createdAt: new Date(), updatedAt: new Date() }); // O sync engine trata o push para cloud (se premium)
Regra 3: App DEVE Funcionar 100% Offline
// ❌ ERRADO - Requer network if (!navigator.onLine) { return <p>You need internet connection</p>; } // ✅ CORRECTO - Funciona offline por defeito const expenses = await db.expenses.toArray(); // Mostrar dados independentemente do estado de conexão // Apenas mostrar indicador de status offline
Regra 4: Sync é Premium Only
// ✅ CORRECTO - Verificar status premium antes de sync import { usePremium } from '@/hooks/use-premium'; const { isPremium } = usePremium(); if (isPremium && navigator.onLine) { await syncEngine.sync(); }
Schema Dexie.js
Localização:
/src/lib/db/index.ts
Estrutura Actual
import Dexie, { Table } from 'dexie'; // Expenses (ExpenseFlow) export interface Expense { id?: number; localId: string; // UUID para sync amount: number; currency: string; category: string; description?: string; date: string; tags?: string[]; isRecurring?: boolean; // Sync metadata syncStatus: 'synced' | 'pending' | 'conflict'; remoteId?: string; // Supabase ID createdAt: string; updatedAt: string; syncedAt?: string; } // FitLog // Ver src/lib/db/fitlog-db.ts export class BreathOfNowDB extends Dexie { expenses!: Table<Expense>; userPreferences!: Table<UserPreferences>; constructor() { super('breathofnow'); this.version(1).stores({ expenses: '++id, localId, date, category, syncStatus', userPreferences: '++id, key' }); } } export const db = new BreathOfNowDB();
State Management com Zustand
Zustand é para UI state apenas, não para persistência de dados:
// ✅ CORRECTO - UI state em Zustand interface AppStore { // Estado de sessão user: User | null; theme: 'light' | 'dark' | 'system'; // UI state isSidebarOpen: boolean; activeApp: string | null; isLoading: boolean; // Actions setTheme: (theme: Theme) => void; toggleSidebar: () => void; } // ❌ ERRADO - Não guardar dados em Zustand interface AppStore { expenses: Expense[]; // NÃO! Usar Dexie transactions: Transaction[]; // NÃO! Usar Dexie }
Sync Engine
Localização:
/src/lib/sync/
Estrutura
src/lib/sync/ ├── index.ts # Exportações principais ├── push.ts # Push de dados locais para cloud ├── pull.ts # Pull de dados da cloud ├── conflict.ts # Resolução de conflitos └── queue.ts # Queue de operações pendentes
Padrão de Uso
import { useSync } from '@/hooks/use-sync'; function MyComponent() { const { syncStatus, lastSyncTime, triggerSync } = useSync(); return ( <div> <SyncStatus status={syncStatus} /> <p>{t('lastSync', { time: lastSyncTime })}</p> <Button onClick={triggerSync}>{t('syncNow')}</Button> </div> ); }
Indicador Offline
// Componente existente: src/components/pwa/connectivity-status.tsx import { ConnectivityStatus } from '@/components/pwa/connectivity-status'; // No layout ou header <ConnectivityStatus />
Padrões de CRUD
Create
async function createExpense(data: ExpenseInput) { const expense = { ...data, localId: crypto.randomUUID(), syncStatus: 'pending' as const, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }; const id = await db.expenses.add(expense); return { ...expense, id }; }
Read
async function getExpenses() { return await db.expenses.toArray(); } async function getExpenseById(localId: string) { return await db.expenses.where('localId').equals(localId).first(); }
Update
async function updateExpense(localId: string, data: Partial<ExpenseInput>) { await db.expenses.where('localId').equals(localId).modify({ ...data, updatedAt: new Date().toISOString(), syncStatus: 'pending' }); }
Delete
async function deleteExpense(localId: string) { // Soft delete para sync await db.expenses.where('localId').equals(localId).modify({ deleted: true, deletedAt: new Date().toISOString(), syncStatus: 'pending' }); }
Checklist de Verificação
Antes de completar qualquer tarefa relacionada com dados:
- Dados são lidos de IndexedDB (Dexie), não de Supabase
- Escritas vão para IndexedDB primeiro
- App funciona 100% offline
- Status de sync é tracked por registo
- Estratégia de resolução de conflitos definida
- Cloud sync está atrás de verificação premium
- Zustand contém apenas UI state, não dados
Benefícios de Privacidade
Esta arquitectura providencia:
- ✅ Data sovereignty: Utilizador é dono dos dados
- ✅ Privacy by default: Dados não saem do dispositivo a menos que optem
- ✅ Offline access: Funcionalidade completa sem internet
- ✅ Performance: Leituras locais instantâneas
- ✅ Controlo: Utilizador pode exportar/apagar todos os dados localmente
Lembra-te: Os dados do utilizador pertencem a eles. Nós estamos apenas a ajudar a organizá-los.