Local notifications need no server: the app schedules them with Android's AlarmManager, and they fire even after the app is closed. BookNest reminds the reader to continue a book, with two action buttons:
export async function remind() {
const perm = await LocalNotifications.requestPermissions(); // Android 13+ asks once
if (perm.display !== 'granted') return show('remind', perm);
await LocalNotifications.registerActionTypes({ types: [{ id: 'READING', actions: [
{ id: 'read', title: 'Read now' }, { id: 'later', title: 'Tomorrow' },
] }] });
await LocalNotifications.schedule({ notifications: [{
id: 3, title: 'Salt and Saffron', body: 'Chapter 4 is waiting for you.',
actionTypeId: 'READING', extra: { bookId: 3 },
schedule: { at: new Date(Date.now() + 5000), allowWhileIdle: true },
}] });
const { notifications } = await LocalNotifications.getPending();
return show('remind', notifications.map(({ id, schedule }) => ({ id, at: schedule.at })));
}The first tap showed the notification permission dialog, and then something less expected: schedule() opened the system's "Alarms & reminders" page for BookNest and waited. allowWhileIdle asks for an exact alarm that fires even in Doze, and Android 14 and later no longer grant SCHEDULE_EXACT_ALARM, which the plugin's manifest declares, by default. checkExactNotificationSetting() had reported {"exact_alarm":"denied"}. After the switch was turned on and Back pressed, the promise resolved, the notification appeared five seconds later (left in the figure of Share), and tapping Read now reached the listener in watchPlugins():
LAB remind [{"id":3,"at":"Thu Sep 24 22:28:22 GMT+08:00 2026"}]
...
LAB notificationAction {"actionId":"read","bookId":3}The listener opened /books/3/. Note that schedule.at came back as Java's Date.toString(), not ISO
Drop allowWhileIdle for reminders
that can be a few minutes late, or call changeExactNotificationSetting() yourself after explaining why. Set a smallIcon in the plugin config, since the default is a generic system icon, and create channels with createChannel() to give users per-type control in Android's settings.