> ## Documentation Index
> Fetch the complete documentation index at: https://docs.presentum.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Storage

> Implement the storage interface for tracking impressions, dismissals, and conversions

## What is storage?

Storage is the persistence layer for tracking presentation events. It records:

* **Impressions** - When presentations are shown
* **Dismissals** - When users close presentations
* **Conversions** - When users take action

Guards and the engine use storage to make decisions about what to show.

<Warning>
  **Storage is optional but highly recommended.** If not provided, Presentum uses `NoOpPresentumStorage` which does nothing and logs warnings (only visible when `presentum.logs` is enabled via `--dart-define=presentum.logs=true`). Without storage, impression tracking, cooldowns, and dismissal states won't persist.

  Always provide a storage implementation in production.
</Warning>

## Storage interface

Implement `PresentumStorage<S, V>`:

```dart theme={null}
abstract interface class PresentumStorage<
  S extends PresentumSurface,
  V extends PresentumVisualVariant
> {
  // Clears specific item by [itemId] on [surface] with [variant] style.
  FutureOr<void> clearItem(
    String itemId, {
    required S surface,
    required V variant,
  });

  // Shown tracking
  FutureOr<void> recordShown(
    String itemId, {
    required S surface,
    required V variant,
    required DateTime at,
  });

  FutureOr<DateTime?> getLastShown(
    String itemId, {
    required S surface,
    required V variant,
  });

  FutureOr<int> getShownCount(
    String itemId, {
    required Duration period,
    required S surface,
    required V variant,
  });

  // Dismissal tracking
  FutureOr<void> recordDismissed(
    String itemId, {
    required S surface,
    required V variant,
    required DateTime at,
  });

  FutureOr<DateTime?> getDismissedAt(
    String itemId, {
    required S surface,
    required V variant,
  });

  // Conversion tracking
  FutureOr<void> recordConverted(
    String itemId, {
    required S surface,
    required V variant,
    required DateTime at,
  });
}
```

## SharedPreferences implementation

Generic production implementation using SharedPreferences, that can be used for any
presentum:

```dart theme={null}
import 'dart:async';

import 'package:presentum/presentum.dart';
import 'package:shared_preferences/shared_preferences.dart';

typedef PersistentPresentumStorageKey<
  S extends PresentumSurface,
  V extends PresentumVisualVariant
> = (String itemId, S surface, V variant);

extension type PersistentPresentumStorageKeys<
  S extends PresentumSurface,
  V extends PresentumVisualVariant
>(PersistentPresentumStorageKey<S, V> key) {
  String get shownCount =>
      '__shown_${key.$1}_${key.$2.name}_${key.$3.name}_count_key__';
  String get lastShown =>
      '__shown_${key.$1}_${key.$2.name}_${key.$3.name}_last_shown_key__';
  String get timestamps =>
      '__shown_${key.$1}_${key.$2.name}_${key.$3.name}_timestamps_key__';
  String get dismissedAt =>
      '__dismissed_${key.$1}_${key.$2.name}_${key.$3.name}_at_key__';
  String get convertedAt =>
      '__converted_${key.$1}_${key.$2.name}_${key.$3.name}_at_key__';

  List<String> get allKeys => [
    shownCount,
    lastShown,
    timestamps,
    dismissedAt,
    convertedAt,
  ];
}

class PersistentPresentumStorage<
  S extends PresentumSurface,
  V extends PresentumVisualVariant
>
    implements PresentumStorage<S, V> {
  PersistentPresentumStorage({required SharedPreferencesWithCache prefs})
    : _prefs = prefs;

  final SharedPreferencesWithCache _prefs;

  @override
  Future<void> clearItem(
    String itemId, {
    required S surface,
    required V variant,
  }) => Future.wait(
    PersistentPresentumStorageKeys((
      itemId,
      surface,
      variant,
    )).allKeys.map(_prefs.remove),
  );

  @override
  FutureOr<DateTime?> getLastShown(
    String itemId, {
    required S surface,
    required V variant,
  }) async {
    final key = PersistentPresentumStorageKeys((
      itemId,
      surface,
      variant,
    )).lastShown;
    final timestampStr = _prefs.getString(key);
    return timestampStr != null ? DateTime.parse(timestampStr) : null;
  }

  @override
  FutureOr<void> recordShown(
    String itemId, {
    required S surface,
    required V variant,
    required DateTime at,
  }) async {
    final keys = PersistentPresentumStorageKeys((itemId, surface, variant));
    final countKey = keys.shownCount;
    final lastShownKey = keys.lastShown;
    final timestampsKey = keys.timestamps;

    final currentCount = _prefs.getInt(countKey) ?? 0;
    final currentTimestamps = _prefs.getStringList(timestampsKey) ?? [];

    await _prefs.setInt(countKey, currentCount + 1);
    await _prefs.setString(lastShownKey, at.toIso8601String());
    await _prefs.setStringList(timestampsKey, [
      ...currentTimestamps,
      at.toIso8601String(),
    ]);
  }

  @override
  FutureOr<int> getShownCount(
    String itemId, {
    required Duration period,
    required S surface,
    required V variant,
  }) async {
    final keys = PersistentPresentumStorageKeys((itemId, surface, variant));
    final timestampsKey = keys.timestamps;
    final timestampStrings = _prefs.getStringList(timestampsKey) ?? [];
    final timestamps = timestampStrings.map(DateTime.parse).toList();
    final cutoff = DateTime.now().subtract(period);
    final count = timestamps.where((t) => t.isAfter(cutoff)).length;

    return count;
  }

  @override
  FutureOr<DateTime?> getDismissedAt(
    String itemId, {
    required S surface,
    required V variant,
  }) async {
    final keys = PersistentPresentumStorageKeys((itemId, surface, variant));
    final timestampStr = _prefs.getString(keys.dismissedAt);
    return timestampStr != null ? DateTime.parse(timestampStr) : null;
  }

  @override
  FutureOr<void> recordDismissed(
    String itemId, {
    required S surface,
    required V variant,
    required DateTime at,
  }) async {
    final keys = PersistentPresentumStorageKeys((itemId, surface, variant));
    await _prefs.setString(keys.dismissedAt, at.toIso8601String());
  }

  @override
  FutureOr<void> recordConverted(
    String itemId, {
    required S surface,
    required V variant,
    required DateTime at,
  }) async {
    final keys = PersistentPresentumStorageKeys((itemId, surface, variant));
    await _prefs.setString(keys.convertedAt, at.toIso8601String());
  }
}
```

[See full production storage ->](https://github.com/itsezlife/presentum/blob/master/example/lib/src/common/presentum/persistent_presentum_storage.dart)

<Info>
  Per-surface-variant tracking lets the same campaign appear differently on
  different surfaces with independent impression counts.
</Info>

## In-memory implementation

For testing or session-only tracking, use built-in [InMemoryPresentumStorage](https://github.com/itsezlife/presentum/blob/748390c7f925a6121dd46447fbfe823dd0bc8a97/lib/src/controller/storage.dart#L70)

## Backend API implementation

Sync events to a backend:

```dart theme={null}
class ApiStorage implements PresentumStorage<AppSurface, CampaignVariant> {
  ApiStorage(this.apiClient);

  final ApiClient apiClient;

  @override
  Future<void> recordShown(
    String itemId, {
    required AppSurface surface,
    required CampaignVariant variant,
    required DateTime at,
  }) async {
    await apiClient.post('/impressions', {
      'item_id': itemId,
      'surface': surface.name,
      'variant': variant.name,
      'timestamp': at.toIso8601String(),
    });
  }

  @override
  Future<DateTime?> getLastShown(
    String itemId, {
    required AppSurface surface,
    required CampaignVariant variant,
  }) async {
    final response = await apiClient.get(
      '/impressions/last',
      params: {
        'item_id': itemId,
        'surface': surface.name,
        'variant': variant.name,
      },
    );

    final timestamp = response['timestamp'] as String?;
    return timestamp != null ? DateTime.parse(timestamp) : null;
  }

  // Implement other methods...
}
```

## No-op storage (default)

If you don't provide a storage implementation, Presentum uses `NoOpPresentumStorage`:

```dart theme={null}
// ⚠️ Without storage - uses NoOpPresentumStorage internally
presentum = Presentum(
  guards: guards,
);
// Logs warnings when storage methods are called
// (only visible with --dart-define=presentum.logs=true)
```

`NoOpPresentumStorage` does nothing and returns default values:

* `getLastShown()` → `null`
* `getShownCount()` → `0`
* `getDismissedAt()` → `null`
* All `record*()` methods → no-op with warning log

<Warning>
  **Don't rely on NoOpPresentumStorage in production.** Without storage, your
  guards can't check impression counts, cooldowns, or dismissal states. Always
  provide a real storage implementation.
</Warning>

## Storage event handler

Use `PresentumStorageEventHandler` to automatically record events:

```dart theme={null}
presentum = Presentum(
  storage: storage, // ✅ Provide storage
  eventHandlers: [
    PresentumStorageEventHandler(storage: storage),
    // This handler calls storage.recordShown/Dismissed/Converted automatically
  ],
  guards: guards,
);
```

[Learn more about events ->](/features/events)

## Best practices

<AccordionGroup>
  <Accordion title="Always provide storage in production">
    Don't rely on `NoOpPresentumStorage`. Always pass a real storage implementation:

    ```dart theme={null}
    // ❌ Bad - uses NoOpPresentumStorage
    presentum = Presentum(guards: guards);

    // ✅ Good - explicit storage
    presentum = Presentum(
      storage: PersistentPresentumStorage(prefs: prefs),
      guards: guards,
    );
    ```
  </Accordion>

  {" "}

  <Accordion title="Handle period-based counts">
    `getShownCount` receives a `period` parameter. Only count impressions within
    that timeframe: `dart final cutoff = DateTime.now().subtract(period); final
          recentCount = timestamps.where((t) => t.isAfter(cutoff)).length; `
  </Accordion>

  <Accordion title="Initialize storage synchronously">
    Avoid async initialization in `initState`. Use `SharedPreferencesWithCache` for synchronous access:

    ```dart theme={null}
    @override
    void initState() {
      super.initState();
      // ✅ Synchronous - no await needed
      _storage = PersistentPresentumStorage(prefs: deps.sharedPreferences);
      presentum = Presentum(storage: _storage);
    }
    ```
  </Accordion>

  <Accordion title="Enable logs during development">
    Use `--dart-define=presentum.logs=true` to see NoOpPresentumStorage warnings and other logs:

    ```bash theme={null}
    flutter run --dart-define=presentum.logs=true
    ```

    This helps catch missing storage implementations during development.
  </Accordion>
</AccordionGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="Guards" href="/core-concepts/guards" icon="shield">
    Use storage in guards
  </Card>

  {" "}

  <Card title="Event system" href="/features/events" icon="bolt">
    Handle events with storage
  </Card>

  <Card title="Implementation guide" href="/guides/implementing-storage" icon="wrench">
    Step-by-step storage building
  </Card>
</CardGroup>
