**T (slot time)** — The local wall-clock time a notification is intended to fire (e.g., 08:00).
**T (slot time)** — The local wall-clock time a notification should fire (e.g., 08:00).
**T–lead** — The moment **{prefetchLeadMinutes} minutes before T** when the system _attempts_ a background prefetch to refresh content.
**T–lead** — The moment **`prefetchLeadMinutes`** before **T** when the system *attempts* a **single** background prefetch. T–lead **controls prefetch attempts, not arming**; locals are pre-armed earlier to guarantee closed-app delivery.
- Example: If T = 12:00 and `prefetchLeadMinutes = 20`, then **T–lead = 11:40**.
- If background prefetch is skipped/denied, delivery still occurs using the most recent cached payload (rolling-window safety).
- T–lead **governs prefetch attempts, not arming**. We still arm one-shot locals early (rolling window) so closed-app delivery is guaranteed.
**Rolling window** — Always keep **today's remaining** (and tomorrow if iOS pending caps allow) locals **armed** so the OS can deliver while the app is closed.
**Rolling window** — Always keep **today’s remaining** one-shot locals armed (and optionally tomorrow, within iOS caps) so the OS can deliver while the app is closed.
**TTL (time-to-live)** — Maximum allowed payload age **at fire time**. If `T − fetchedAt > ttlSeconds`, we **skip** arming for that T.
**TTL (time-to-live)** — Maximum allowed staleness of a payload at **fire time**. If the projected age at T exceeds `ttlSeconds`, we **skip** arming.
**Shared DB (default)** — The app and plugin open the **same SQLite file**; the app owns schema/migrations, the plugin performs short writes with WAL.
**WAL (Write-Ahead Logging)** — SQLite journaling mode that permits concurrent reads during writes; recommended for foreground-read + background-write.
**`PRAGMA user_version`** — An integer the app increments on each migration; the plugin **checks** (does not migrate) to ensure compatibility.
**Exact alarm (Android)** — Minute-precise alarm via `AlarmManager.setExactAndAllowWhileIdle`, subject to policy and permission.
**Storage (single shared DB):** The app and the native plugin will use **the same SQLite database file**. The app owns schema/migrations; the plugin opens the same file with WAL enabled and performs short, serialized writes. This keeps one source of truth for payloads, delivery logs, and config.
### SQLite Ownership & Concurrency
* **One DB file:** The plugin opens the **same path** the app uses (no second DB).
* **Migrations owned by app:** The app executes schema migrations and bumps `PRAGMA user_version`. The plugin **never** migrates; it **asserts** the expected version.
* **WAL mode:** Open DB with `journal_mode=WAL`, `synchronous=NORMAL`, `busy_timeout=5000`, `foreign_keys=ON`. WAL allows foreground reads while a background job commits quickly.
* **Single-writer discipline:** Background jobs write in **short transactions** (UPSERT per slot), then return.
* **Encryption (optional):** If using SQLCipher, the **same key** is used by both app and plugin. Do not mix encrypted and unencrypted openings.
### Scheduling & T–lead
- **Arm** a rolling window (today + tomorrow within iOS cap).
**T–lead governs prefetch, not arming.** We **arm** one-shot locals as part of the rolling window so closed-app delivery is guaranteed. At **T–lead = T − prefetchLeadMinutes**, the **native background job** attempts **one** 12s ETag-aware fetch. If fresh content arrives and will not violate **TTL-at-fire**, we (re)arm the upcoming slot; if the OS skips the wake, the pre-armed local still fires with cached content.
**TTL-at-fire:** Before arming for time **T**, compute `T − fetchedAt`. If that exceeds `ttlSeconds`, **do not arm** (skip). This prevents posting stale notifications when the app has been closed for a long time.