Skip to content

Add a marketing tool API with marketer keys and audience queries - #78

Open
peachbits wants to merge 9 commits into
masterfrom
matthew/marketing-push-tool
Open

Add a marketing tool API with marketer keys and audience queries#78
peachbits wants to merge 9 commits into
masterfrom
matthew/marketing-push-tool

Conversation

@peachbits

@peachbits peachbits commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

CHANGELOG

Does this branch warrant an entry to the CHANGELOG?

  • Yes
  • No

Dependencies

none

Description

Adds the marketing tool API consumed by the web UI in EdgeApp/edge-internal-tools#1, in three pieces:

  • DB helpers: an apiKeyLocation view keyed [apiKey, country, region, city] with a streaming reader, plus a batched-by-id device lookup.
  • Marketer keys: API keys gain a marketer flag and a targetApiKeys list. The marketing endpoints require the flag (or admin) 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.
  • The API: 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), and POST /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>"] } in db_api_keys (no adminsdk needed — 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/device endpoint: 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. locationFilter has 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 /marketing for 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), and POST /marketing/push (send to an explicit device-id list from a prior query, with confirmed and streaming plain-text progress).

Access control is tightened via API key fields marketer and targetApiKeys: only marketer (or admin) keys can call these routes, and sends only reach devices registered under keys in targetApiKeys—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 apiKeyLocation view and streamDevicesByApiKeyLocation, batched fetchDevicesByIds, and cleaners/types for the new key fields. Location filtering and device skip rules (opt-out, token validity, key scoping) live in locationFilter with mocha tests.

Reviewed by Cursor Bugbot for commit 29f2e40. Bugbot is set up for automated code reviews on this repo. Configure here.

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.
Comment thread src/server/marketing/marketingTool.ts Outdated
Comment thread src/db/couchDevices.ts
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
peachbits force-pushed the matthew/marketing-push-tool branch from e87a23a to d2f9ec5 Compare July 30, 2026 23:10
@peachbits peachbits changed the title Add marketing tool API endpoints Add a marketing tool API with marketer keys and audience queries Jul 30, 2026
Comment thread src/server/marketing/marketingTool.ts Outdated
Comment thread src/server/marketing/marketingTool.ts

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Comment thread src/server/marketing/marketingTool.ts
Comment thread src/server/marketing/marketingTool.ts

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread src/server/index.ts
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant