// ── Core's own push-notification streams ─────────────────────────────────── // // What is left of config/notificationStreams.js once the shard-derived catalog // moved to config/shardStreams.js (MODULE_SYSTEM.md §1.8: push INFRASTRUCTURE is // core, the CATALOG is content). `news.post` is produced by the website's own // posts path, not by any game feed, and the four `team.*` streams by core's own // Team sync and forum. // // Registered through modules/registries.js like any module's, and read back // through it — nothing imports this file to get "the catalog", because the // catalog is core's plus every module's. // // Phase 6 added the four Team streams below. They are core's for the same reason // the Team tables are: a module supplies who is in a Team, but who may be told // about it is the access resolver's answer, and that is core's (TEAMS.md Part 6). // // The payload that ever leaves the server is a CONTENT-FREE tickle // ({ stream, ref }); the app wakes and PULLS the real, ownership-checked content // over the authenticated API (docs/android/PLAN.md §11). const STREAMS = [ { id: 'news.post', label: 'News posts', description: 'New news / Five-on-Friday / newsletter posts.', personal: false, requiresLinkedAccount: false, }, // ── Teams (TEAMS.md §6.2, phase 6) ─────────────────────────────────────── // // FOUR streams, and not one per Team. The catalog is a static registration // validated at boot; it has no way to express an unbounded runtime-created set, // and a stream id per Team would leave rows in notification_subscriptions to // collect every time a Team archived. Which Team an event came from lives in // the RECIPIENT SET (utils/teamNotify.js) and in the `ref`, never in the id. // // `requiresLinkedAccount: false` on all four is deliberate and reads oddly. // These are game-sourced events, so the instinct is to demand a linked game // account — but a forum-granted user with no game identity at all is exactly // the population §2.5 path 3 exists for, and they are a legitimate recipient of // `team.forum.post`. The flag would refuse them a toggle they have every right // to. What enforces who gets what is the recipient computation, which asks the // access resolver; the stream flag is not a second, weaker copy of that rule. // // `personal: false` for the same reason it is false on news.post: these are not // owner-keyed events about one account's own property. `publishToUsers` is a // third fan-out shape alongside "everyone subscribed" and "this one owner", and // the catalog has no flag for it because the flag would say nothing a caller // does not already know by choosing the function. { id: 'team.member.joined', label: 'Team — new member', description: 'Someone joined a Team you belong to.', personal: false, requiresLinkedAccount: false, }, { id: 'team.leadership.changed', label: 'Team — leadership change', description: 'Leadership changed in a Team you belong to.', personal: false, requiresLinkedAccount: false, }, { id: 'team.forum.post', label: 'Team — new forum post', description: 'A new thread or reply in a Team forum you can read.', personal: false, requiresLinkedAccount: false, }, { id: 'team.announcement', label: 'Team — announcements', description: 'A leader posted an announcement in a Team you can read.', personal: false, requiresLinkedAccount: false, }, // ── The event system (EVENTS.md §J — Phase 10) ────────────────────────── // // **One of the seven `event.` triggers is also a stream, and that is a // decision rather than an oversight** (org lead, 2026-09-04). A stream is a // PUSH toggle: `notificationChannelPrefs.catalog` offers the push channel only // for ids registered here, `publishToUsers` joins `notification_subscriptions`, // and that table is only ever written for a channel a user could switch on. So // a trigger that is not also a stream can be mailed and put in the inbox, and // its push is dead — a tickle published to nobody, which the send log // nonetheless records as sent. Found on the live rig; the seeded rule named // `push` before this line existed. // // **`run.started` alone, because push is the channel that says "now".** It is // the one lifecycle moment worth waking a phone for — ENGAGEMENT.md §8.5's // *"come back for X"* — and the other six are things a player reads when they // next look. Six more toggles would put a wall of switches on the preferences // screen for one feature, and `event.phase.changed` is the one most likely to // buzz a phone four times in an evening. // // Same id as the trigger, which is §7.2's one namespace and the same-owner // upgrade `news.post` already is: one id, one owner, two facets. { id: 'event.run.started', label: 'Events — starting now', description: 'A scheduled event is beginning.', // Not owner-keyed: this is a public event happening in public, not a fact // about one account's own property. Same as `news.post`. personal: false, // A player with no linked game account can still want to know an event is on. requiresLinkedAccount: false, }, ] module.exports = { STREAMS }