Add a marketing tool API with marketer keys and audience queries - #78
Open
peachbits wants to merge 9 commits into
Open
Add a marketing tool API with marketer keys and audience queries#78peachbits wants to merge 9 commits into
peachbits wants to merge 9 commits into
Conversation
Document the server config shape in a committed sample file, since the real pushServerConfig.json is gitignored. Copy it to pushServerConfig.json and fill in real values to run the server.
6 tasks
The marketing tool needs to walk the devices registered under one app key within one country, and to re-read an explicit list of devices when sending. Add an `apiKeyLocation` view keyed by [apiKey, country, region, city] with a streaming reader, plus a batched-by-id lookup that skips missing or unreadable documents.
The keys baked into the apps ship to every phone, so they make poor credentials for anything beyond device registration, and they cannot rotate without an app release. Give API keys a `marketer` flag that will gate the marketing endpoints, and a `targetApiKeys` list naming the app keys whose devices a marketing key may reach. Both fields default to harmless values, so existing key documents stay valid.
Three endpoints under `/marketing`, consumed by the web UI in the internal tools project, which supplies the API key: `test` sends a single push to one device or login, `query` lists the devices a send would reach, and `push` sends to the device list a query returned. Queries filter by include and exclude lists of cities and regions: the lines within a list are alternatives, the lists narrow each other, and matching is case-insensitive. The send takes device ids rather than a location so the caller blasts exactly the audience it reviewed, and so a server missing this route fails loudly instead of resolving the same body to a different audience. Every endpoint requires a key with the `marketer` (or `admin`) flag and only reaches devices under the key's `targetApiKeys`, enforced by one shared skip predicate at query and send time. The router parses its own bodies ahead of the app-wide 1mb parser, since a device-id list can run to several megabytes.
peachbits
force-pushed
the
matthew/marketing-push-tool
branch
from
July 30, 2026 23:10
e87a23a to
d2f9ec5
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit a769049. Configure here.
Firebase reports an uninstalled app as `Error: NotRegistered`, or as a `messaging/registration-token-not-registered` code on the newer API, but the daemon only recognized an older wording. So the branch that clears a dead token never ran, and every send retried every dead token. A marketing send to 385,616 devices measured this: 208,892 of them, 54%, failed on unregistered tokens, and not one was disabled. Dry-run probes against a sample of production tokens put the dead share at a similar 62%, so roughly half of every send is spent on devices that cannot receive anything, and that share only grows. Match all three spellings, in a tested helper rather than inline, since this is the second time the wording has moved out from under the check. Also pass the device and error to Pino as its first argument. Passing them second silently dropped them, so all 208,892 failures logged as a bare "Unknown error" with nothing to diagnose.
A send to a few hundred thousand devices spent over a minute reading documents without printing anything, then emitted a heartbeat line per device once it started queueing. Neither told the operator how far along the send was or how much longer it would take. Report a percentage every thirty seconds instead, with a rate and an estimate of the time left, for both the loading and the queueing pass. Loading now streams a batch at a time so it can report at all, and unusable tokens are counted rather than logged one per line, which used to bury everything else. Rename the closing line from "Marketing send complete" to "N devices queued for delivery". The send hands messages to the queue and the publish daemon delivers them afterwards, so for a large audience the old wording claimed success while most of the work had not happened yet.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

CHANGELOG
Does this branch warrant an entry to the CHANGELOG?
Dependencies
none
Description
Adds the marketing tool API consumed by the web UI in EdgeApp/edge-internal-tools#1, in three pieces:
apiKeyLocationview keyed[apiKey, country, region, city]with a streaming reader, plus a batched-by-id device lookup.marketerflag and atargetApiKeyslist. The marketing endpoints require the flag (oradmin) and only reach devices registered under the listed keys — the api keys baked into the apps cannot call them, a marketing key can be rotated without an app release, and a key with no targets fails closed.POST /marketing/test(single test push),POST /marketing/query(lists the devices a send would reach, filtered by include/exclude lists of cities and regions — lines within a list are alternatives, the lists narrow each other, matching is case-insensitive), andPOST /marketing/push(sends to the device list a query returned). The send takes device ids rather than a location so the caller blasts exactly the audience it reviewed, and so a stale server fails loudly (unknown route) instead of resolving the same body to a different audience.Operational notes: the key doc for the tool needs
{ marketer: true, targetApiKeys: ["<app api key>"] }indb_api_keys(noadminsdkneeded — delivery credentials resolve from each device's own key), and the marketing router parses its own bodies with a 10mb limit ahead of the app-wide 1mb parser.Tested against a local CouchDB with devices registered through the real
/v2/deviceendpoint: include/exclude query counts, tenant scoping (devices under a second app key never matched), the auth matrix (app key 403, unknown key 401, target-less marketer key 400), and queue publishes for exactly the queried device list.locationFilterhas a mocha suite covering the filter and skip-reason logic.Note
Medium Risk
New authenticated bulk-push surface and large-body handling; scoping and marketer flags reduce blast radius, but misconfigured targetApiKeys or operator mistakes could still reach large audiences.
Overview
Adds a marketing tool HTTP API under
/marketingfor the internal tools UI:POST /marketing/test(single-device or login test push),POST /marketing/query(audience preview by country plus city/region include/exclude lists), andPOST /marketing/push(send to an explicit device-id list from a prior query, withconfirmedand streaming plain-text progress).Access control is tightened via API key fields
marketerandtargetApiKeys: only marketer (oradmin) keys can call these routes, and sends only reach devices registered under keys intargetApiKeys—app-embedded keys cannot blast the whole audience. Auth runs before body parsing; the marketing router uses a 10mb JSON limit mounted ahead of the global 1mb parser.CouchDB gains an
apiKeyLocationview andstreamDevicesByApiKeyLocation, batchedfetchDevicesByIds, and cleaners/types for the new key fields. Location filtering and device skip rules (opt-out, token validity, key scoping) live inlocationFilterwith mocha tests.Reviewed by Cursor Bugbot for commit 29f2e40. Bugbot is set up for automated code reviews on this repo. Configure here.