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.md
source 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.