# Local data includes metadata (timestamp, source)

- **Pattern:** `ab-001890` (`mobile-offline-storage.data-persistence.data-metadata`)
- **Severity:** low
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-001890
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-001890)

## Why it matters

Stored records without timestamps or source markers make it impossible for the app to determine whether cached data is fresh, which copy won a conflict, or whether a local edit has been synced. ISO 25010:2011 reliability.recoverability requires that the system can determine the state of data after a failure — impossible without creation and modification timestamps. Without a `source` marker, merge logic cannot distinguish local-only edits from server-authoritative records. This undermines every higher-order pattern in this bundle: cache invalidation, conflict resolution, and sync-on-reconnect all require metadata to make correct decisions.

## Severity rationale

Low because missing metadata degrades offline correctness and merge reliability rather than enabling direct exploitation, but it undermines every other sync-safety pattern.

## Remediation

Add `createdAt`, `updatedAt`, `source`, and `version` fields to every data model written to local storage. Populate them at the point of save, not at the point of display.

```tsx
interface StoredRecord {
  id: string;
  // ... domain fields ...
  createdAt: number;          // Unix ms
  updatedAt: number;          // Unix ms
  source: 'local' | 'remote'; // origin of last write
  version: number;            // schema version for migrations
}

export async function saveRecord(
  record: Omit<StoredRecord, 'createdAt' | 'updatedAt' | 'source' | 'version'>
) {
  const now = Date.now();
  const stored: StoredRecord = { ...record, createdAt: now, updatedAt: now, source: 'local', version: 1 };
  await AsyncStorage.setItem(`record_${record.id}`, JSON.stringify(stored));
}

export async function updateRecord(id: string, patch: Partial<StoredRecord>) {
  const existing = await loadRecord(id);
  if (!existing) return;
  await AsyncStorage.setItem(
    `record_${id}`,
    JSON.stringify({ ...existing, ...patch, updatedAt: Date.now() })
  );
}
```

Review all existing `AsyncStorage.setItem` call sites and add the four metadata fields to any schema that stores mutable domain data.

## Detection

- **ID:** `data-metadata`
- **Severity:** `low`
- **What to look for:** Examine stored data structures. Look for timestamps, source information (local vs. remote), version markers, or other metadata attached to stored records that help the app understand data freshness and origin.
- **Pass criteria:** Count all data model definitions or schema declarations. Stored data includes at least 2 metadata fields such as creation timestamp, last modified timestamp, source marker (local vs. remote), or version number. These metadata fields help the app make intelligent cache/merge decisions.
- **Fail criteria:** Stored data has no metadata. App cannot determine if cached data is stale or distinguish between local and remote origin. No more than 0 primary data models should lack timestamp fields.
- **Skip (N/A) when:** Never — metadata is valuable for robust offline handling.
- **Detail on fail:** `"Stored records have no timestamps. App cannot determine data freshness or decide when to refresh from server."`
- **Remediation:** Add metadata to your data model:

  ```tsx
  interface StoredRecord {
    id: string;
    title: string;
    description: string;
    // Metadata
    createdAt: number; // Unix timestamp
    updatedAt: number; // Unix timestamp
    source: 'local' | 'remote'; // Where did this data originate?
    version: number; // Track schema version for migrations
  }

  export async function saveRecord(record: Omit<StoredRecord, 'createdAt' | 'updatedAt' | 'source' | 'version'>) {
    const now = Date.now();
    const stored: StoredRecord = {
      ...record,
      createdAt: now,
      updatedAt: now,
      source: 'local',
      version: 1,
    };

    await AsyncStorage.setItem(`record_${record.id}`, JSON.stringify(stored));
  }

  export async function updateRecord(id: string, updates: Partial<StoredRecord>) {
    const existing = await getRecord(id);
    if (!existing) return;

    const updated: StoredRecord = {
      ...existing,
      ...updates,
      updatedAt: Date.now(),
      version: existing.version,
    };

    await AsyncStorage.setItem(`record_${id}`, JSON.stringify(updated));
  }

  export async function mergeRemoteRecord(remote: any) {
    const local = await getRecord(remote.id);

    if (!local) {
      // New remote record
      const stored: StoredRecord = {
        ...remote,
        createdAt: remote.createdAt || Date.now(),
        updatedAt: remote.updatedAt || Date.now(),
        source: 'remote',
        version: 1,
      };
      await AsyncStorage.setItem(`record_${remote.id}`, JSON.stringify(stored));
      return;
    }

    // Merge: prefer most recently updated
    const merged: StoredRecord = {
      ...local,
      ...(remote.updatedAt && remote.updatedAt > local.updatedAt ? remote : {}),
      updatedAt: Math.max(local.updatedAt, remote.updatedAt || 0),
      source: local.source, // Keep original source tracking
      version: Math.max(local.version, remote.version || 1),
    };

    await AsyncStorage.setItem(`record_${merged.id}`, JSON.stringify(merged));
  }

  // Cleanup old data based on metadata
  export async function cleanupStaleData(maxAgeMs = 30 * 24 * 60 * 60 * 1000) {
    const allKeys = await AsyncStorage.getAllKeys();
    const now = Date.now();

    for (const key of allKeys) {
      if (!key.startsWith('record_')) continue;

      const data = await AsyncStorage.getItem(key);
      if (!data) continue;

      const record = JSON.parse(data) as StoredRecord;
      if (now - record.updatedAt > maxAgeMs) {
        await AsyncStorage.removeItem(key);
      }
    }
  }
  ```

## External references

- iso-25010 reliability.recoverability

Taxons: data-integrity

HTML version: https://auditbuffet.com/patterns/ab-001890
