The practice API gained GET /arrivals?after=<id>, POST /push-tokens for the app's token, and a test hook, POST /arrivals, that adds a book. In production the server would push to every stored token; BookNest also checks whenever it comes to the foreground and turns each new book into a local notification:
let pending: Promise<void> | null = null;
export const checkArrivals = () => (pending ??= check().finally(() => { pending = null; }));
async function check() {
const after = prefs.getNumber('lastArrival') ?? 60;
const { books } = (await (await fetch(`${API}/arrivals?after=${after}`)).json()) as
{ books: Book[] };
for (const b of books) {
await Notifications.scheduleNotificationAsync({
// ...title 'New at BookNest', data: { url: `/book/${b.id}` }, trigger: { channelId }
});
prefs.set('lastArrival', b.id);
}The shared pending promise fixes a bug the first run showed: the startup check and an AppState "active" event (after the permission dialog) ran together, which would have announced a new book twice. After curl 3,008 -X POST localhost:8150/arrivals, bringing BookNest to the front and tapping the notification:

'arrivals after', 60, ':', 1 'in the foreground:', 'New at BookNest' 'tapped:', '/book/61', '| action:', 'expo.modules.notifications.actions.DEFAULT' 'path:', '/book/61' 'arrivals after', 61, ':', 0
The tap opened the new book's screen; lastArrival in MMKV 18,748 had moved to 61.