---
name: local-first
description: 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.
---
# 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
```typescript
// ❌ 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
```typescript
// ❌ 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
```typescript
// ❌ ERRADO - Requer network
if (!navigator.onLine) {
return
You need internet connection
;
}
// ✅ 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
```typescript
// ✅ 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
```typescript
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;
userPreferences!: Table;
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:
```typescript
// ✅ 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
```typescript
import { useSync } from '@/hooks/use-sync';
function MyComponent() {
const { syncStatus, lastSyncTime, triggerSync } = useSync();
return (
{t('lastSync', { time: lastSyncTime })}
);
}
```
## Indicador Offline
```typescript
// Componente existente: src/components/pwa/connectivity-status.tsx
import { ConnectivityStatus } from '@/components/pwa/connectivity-status';
// No layout ou header
```
## Padrões de CRUD
### Create
```typescript
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
```typescript
async function getExpenses() {
return await db.expenses.toArray();
}
async function getExpenseById(localId: string) {
return await db.expenses.where('localId').equals(localId).first();
}
```
### Update
```typescript
async function updateExpense(localId: string, data: Partial) {
await db.expenses.where('localId').equals(localId).modify({
...data,
updatedAt: new Date().toISOString(),
syncStatus: 'pending'
});
}
```
### Delete
```typescript
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.**