- Inherited: Using the project’s default setting.
- Overridden: Explicitly set by the recipient.
Preferences are per medium
Each preference applies to a single medium. Thein_app and email preferences for the same target are toggled independently, so a recipient can keep the in-app bell on while turning email off.
- Project-level preferences form the catalog: they set the default for a
(target, medium). Onlyin_appandemailcan be cataloged today. - Recipient-level preferences are the per-recipient opt-in/opt-out for a
(target, medium).
medium (in_app or email). It defaults to in_app when omitted, so existing integrations keep working unchanged.
Strict targets
Strict targets is a per-project setting that turns the catalog into a gate: with it on, a send whose(target, medium) has no project-level preference is rejected with 400 and nothing is written.
Why off by default
Strictness is a maturity setting, not a correctness rule. On from the start, your very first targeted send fails with “create a project preference for it before sending” — at the moment you have built nothing and have no reason to know what a catalog is. That is a wall, not a guardrail. The order that works is the order you will naturally take:- Send something untargeted. No setup at all.
- Add a
target, because you want per-notification analytics. - Discover preferences, because you want recipients to be able to mute it.
- Seed the catalog from your deploy, so it stops drifting from your code.
- Turn strict targets on, once your catalog is stable and you want a typo to fail loudly.
When to turn it on
Turn it on once your catalog is seeded from your deploy pipeline and you would rather a bad send fail than succeed quietly. What you get is the difference between a bug found on the first call and one found the week someone asks why they were never notified. The alternative — accepting the send and recording it as muted — hands you a200 for a notification that will never reach a human, and makes “the recipient opted out” indistinguishable from “you typo’d the event name”. So Bodhveda rejects rather than muting.
Leaving it off has a real cost worth naming: an uncataloged target has no preference surface. It appears on no settings screen, so a recipient cannot discover it, let alone switch it off. Off does not mean no consequences, it means the consequences are silent.
Your console’s project settings list the targets you have sent but not cataloged, so turning strict targets on is never a blind switch — you can see exactly what would start failing.
What the gate checks
The gate checks existence, not
default_enabled. A cataloged entry with default_enabled: false is a real state — defined, currently switched off — so sending to it is accepted and simply reaches nobody. Only a missing entry is an error.payload needs an in_app entry; one carrying only email needs an email entry; one carrying both needs both.
It applies to direct sends and broadcasts alike. With strict targets off, a broadcast to an uncataloged target is accepted and reaches the recipients who explicitly subscribed to it, if any; the rest are reported as excluded_not_cataloged in the broadcast’s audience breakdown.
A send with no target at all is never gated, whatever the setting — it claims no target, so there is nothing to check it against.
How a preference resolves
Whether a target delivers is resolved, not read from a single row. The first rule that matches wins:- A mandatory catalog entry for the target (see below).
- The recipient’s rule for the exact target.
- The recipient’s
topic: anyrule for that channel/event. - The project’s rule for the exact target (the catalog).
- The project’s
topic: anyrule. - The medium’s default — in-app delivers, every other medium does not.
topic: none target never takes an any rule — none means “this rule has no topic”.
The preference read reports state.cataloged and state.mandatory separately from state.enabled. Use them to decide what to render on a settings screen; use enabled to know what will actually happen.
Wildcards satisfy the gate
Atopic: any catalog entry matches every concrete topic under that channel and event — for the gate exactly as for resolution. One entry covers an unbounded set of runtime-generated topics.
This is what makes per-resource muting practical. Catalog comments/any/reply once, then send comments/post_812/reply, comments/post_913/reply, and so on. Every send passes the gate, and a recipient rule on one exact target mutes that one thread and nothing else — no state on your side.
Mandatory entries
Some notifications must not be optional: password resets, security alerts, billing failures, a one-shot welcome. But everything in the catalog is normally something a recipient can switch off. Setmandatory: true on a catalog entry to resolve that. The entry exists, so sends pass the gate; the recipient’s toggle is refused with a 400, and any rule they already had is ignored.
Mandatory is independent of strict targets. It is a property of the catalog entry, not of the gate, so it behaves identically whether or not strict targets is on: the entry is what makes the target non-negotiable, and cataloging it is what you had to do anyway to give recipients a switch you then refuse to honour.
default_enabled still applies to a mandatory entry, so setting it false stops the notification. Mandatory removes the recipient’s choice, not yours.
Seed the catalog from your deploy
Your desired catalog lives in your code; the real one lives in Bodhveda. They drift the moment you ship a new event and forget to update Bodhveda to match. Derive the catalog from whatever already defines your events, and push it with upsert preferences as a step in your deploy pipeline rather than a one-time setup task. It is an idempotent merge, so a deploy that changes nothing is a no-op. This is worth doing before you turn strict targets on, and it is worth doing even if you never do. With the gate off, drift is silent: the notification sends, but the target is on no settings screen, so no recipient can mute it. Run it on every deploy, not just the deploy that adds an event. A catalog seeded by hand is a catalog that falls behind the code that depends on it. If you want to build something like Reddit where an author is subscribed to their own post and any other user can explictly follow someone else’s post as well.-
For the author, you can send a direct notification and it will always deliver unless the recipient explicity unsubscribe to that target. You should still use
targetso that you get analytics based on targets. - For the user who wants to follow someone else’s post, you should subscribe the recipient to that target and if they unfollow the post, unsubscribe them from that target.
- You could subscribe the author to their post’s target so that whenever some event is triggered on that post, you can just send a broadcast notification and the author and the other user both will recieve the notification.