Send push notifications

Prompt:

Let our backend send push notifications to the users of our iPhone and Android app.

Also works: "Notify a user when their order ships." "Push to every device a user has signed in on." "Stop sending to phones that uninstalled the app."

What your tool does

  • Reads your manifest. Where its services array declares no push service, it adds {"kind": "push"} and resubmits the manifest with submit_manifest.
  • Stores each provider's credential by calling store_secret with the application, the environment, and the credential's file as value_file. It runs the command line the call returns (Store a secret), so the value never passes through a tool call and is never shown again.
  • Calls configure_push with the Apple and Google members, naming the secrets it stored, once for each environment the application has.
  • Calls read_push to confirm the configuration, the registered devices, and the month's count against your plan.
  • Writes the app's registration call: a PUT to the device route of the realm after sign-in, with the device token from the phone's operating system.
  • Writes the backend's send with the Push package's client, under the platform credential, naming the users to reach.
  • Tests the send over the Push package's test double, so no hosted environment is needed.
  • Asks you for the Apple team and key identifiers, the bundle identifier, and the Firebase project where the prompt does not give them.

What you need

  • An app whose users sign in through the realm (Sign in from a native app), so each device is registered under a user.
  • For iPhone: an Apple Push Notifications signing key, the .p8 file, with its key identifier, your team identifier, and the app's bundle identifier.
  • For Android: a Firebase project with a service-account JSON file that may send messages.

Before your AI starts

This section is for your AI tool: what it checks and gathers before it begins. You don't need to do these steps yourself.

  • A connected, signed-in tool (Connect your tool).
  • An application whose manifest declares the accounts service and the push service, submitted.
  • The two credential files on the developer's machine, read once by the command line of Store a secret and never into a chat or a tool call.

Steps

1. Store the credentials as secrets

Each provider credential is stored as a secret of the application in one environment. Store the Apple key's text and the Google file's text under names of your choosing. Your tool calls store_secret with each file as value_file and runs the command line the call returns, because the store_secret tool takes no value. Store a secret describes the line. The value is written once and never redisplayed. The command sends a body of this form. No test checks this sample.

{
  "application": "<application id>",
  "environment": "production",
  "name": "apns-key",
  "value": "<the .p8 file's text>"
}

Store the Google service-account file the same way, under a name such as fcm-service-account. Each stored name has one environment's value, so where the application has a development environment, its copies take names of their own.

2. Declare the push service

Add {"kind": "push"} to the manifest's services and resubmit it. The declaration provisions nothing by itself; it makes the configuration and the routes available. The receipt lists the next two steps.

The entry can also name its providers, apns and fcm, as step 3's call does without Apple's environment. Each submission then configures each environment the application has, and create_environment configures development when it adds it, so step 3 is needed only to change Apple's gateway. Author the manifest shows the entry.

3. Configure the providers

Call configure_push once for each environment the application has, naming the secrets you stored. Each provider is optional, and a provider set to null is removed. The platform checks that each name is present at that environment's scope, and moves nothing. No test checks this sample.

{
  "application": "<application id>",
  "environment": "production",
  "apns": {
    "team_id": "ABCDE12345",
    "key_id": "KEYID12345",
    "bundle_id": "com.example.app",
    "key_secret_name": "apns-key",
    "environment": "production"
  },
  "fcm": {
    "project_id": "example-app-12345",
    "service_account_secret_name": "fcm-service-account"
  }
}

Apple's environment selects the gateway: sandbox for a debug build, production for a store build. On an application with one environment, both builds register on the production realm, whose configuration names one gateway at a time. Name production for the store build, or sandbox while you test a debug build. A name bound to an upstream declaration or to a realm sign-in method is refused, and a name a push configuration uses cannot be given to those afterwards. read_push returns the configuration with the names alone.

While the manifest's push entry names a provider, configure_push refuses to change that provider's members or remove it, manifest_owned_field. It still sets Apple's gateway for each environment.

4. Register the device from the app

After sign-in, the app asks the device's operating system for a device token and registers it under the signed-in user. Send a PUT to /__account/devices/<installation id> on the application's own hostname, with the bearer token or the cookie from sign-in. The app creates the installation identifier once and keeps it. No test checks this sample.

{
  "platform": "ios",
  "token": "<the device token as the platform handed it>",
  "client_id": "com.example.app"
}

Include client_id only where the app signed in through a native client the realm declares (Sign in from a native app). An undeclared one is refused invalid_client, and a registration may leave client_id out. A registration with a platform whose provider is not configured is kept, and sends reach it once that provider is configured.

Register again whenever the operating system issues a new token. A user can have at most twenty registrations, and a twenty-first ends the oldest. A token another user registers moves to that user. A DELETE on the same route removes a registration. A registration also ends with the session it was made under: sign-out, a revoke from the session list, or revoke_end_user removes it, and the app registers again after the next sign-in. A user's deletion removes them all, with the user's deliveries still waiting and the records of those already sent.

5. Send from the backend

The Push package's client makes this call for the backend. Build it once from TURNZERO_CLOUD_API and TURNZERO_CLOUD_TOKEN, then call send(users, message) or sendToDevices(installations, message). It maps the message onto the body below and throws a typed refusal that identifies anything the platform refused.

Post to /push/v0/messages under the platform credential, the value of TURNZERO_CLOUD_TOKEN. The credential identifies the application and sets the environment, so the body contains neither. Name the users to reach, or the installations. No test checks this sample.

{
  "to": { "users": ["<user id>", "<user id>"] },
  "notification": { "title": "Your order shipped", "body": "Arriving Tuesday.", "badge": 1, "sound": "default" },
  "data": { "order_id": "A-1042" },
  "ttl_seconds": 86400,
  "collapse_id": "order-A-1042",
  "priority": "high"
}

A push may include these members:

  • notification: a title of up to 256 characters, a body of up to 4,000, and an optional badge count and sound name.
  • data: a map of string values for the app, under 4 kilobytes serialized. The key aps is reserved.
  • ttl_seconds: how long the provider keeps the notification for an offline device, a day where absent and four weeks at most.
  • collapse_id: a key under which a newer notification replaces an older one on the device.
  • priority: high where absent, or normal to let the device batch it.

One send can list up to 500 users or installations. A device whose platform has no configured provider is not accepted. Each accepted delivery counts toward the plan's monthly push allowance, and a send that would pass it is refused whole (Plan and usage). A send that accepts no device is never refused for the allowance.

Expected result

The send returns 202 with a message_id, the count of deliveries accepted, and the users or installations that had no registration. A delivery pass hands each accepted delivery to its provider within seconds, retries a transient failure twice, and marks a device the provider reports gone. Such a device receives nothing more until the app registers it again.

read_push returns the device counts by platform, the deliveries of the last hour by outcome, and the month's accepted count beside the plan's quantity. read_usage returns the same count under push_messages, and the send that first passes the warning fraction of the quantity writes one usage warning line into the application's log stream for each environment. The application's log stream contains one line per delivery under the counters push_delivered and push_failed (Read logs and counters). A failed line gives its failure and a provider_reason, which is one of three words, never the provider's own message:

  • provider_refused: the provider refused the notification or its provider token through the last attempt, or reported the device gone.
  • provider_unreachable: the provider could not be reached, or kept returning a transient failure, through the last attempt.
  • credential_unreadable: the provider credential named in the configuration could not be read or is not a usable key or file. Store it again with store_secret, as Store a secret describes, and call configure_push again.

Refusals

Where a push entry in the manifest sets the providers, name_bound_to_setting comes from submit_manifest or create_environment instead. There the manifest stays recorded, and the remedy is a new name in the manifest or the binding removed (Author the manifest).

Refusal Status Cause Remedy
push_not_declared 409 configure_push or read_push on an application whose manifest declares no push service. Add {"kind": "push"} to the manifest's services and resubmit it.
custody_entry_missing 409 A named secret is not stored at that environment's scope. Store it with store_secret, as Store a secret describes, naming the application and the environment, then configure again.
name_bound_to_upstream 409 The name is an upstream declaration's credential. Store the credential under another name and configure with that name.
name_bound_to_realm 409 The name is a realm sign-in method's credential. Store the credential under another name and configure with that name.
name_bound_to_setting 409 A setting binds the name: the manifest's settings, the running copy of either environment, or a deploy, promote, or redeploy in flight to one. The detail names the setting and where it is bound. Store the credential under another name and configure with that name. A running copy keeps the binding until that environment's next deploy or promote of a manifest without it. A deploy, promote, or redeploy in flight keeps it until it ends.
name_bound_to_push 409 declare_upstream or configure_realm named a push credential. Give the upstream or the realm a credential of its own.
manifest_owned_field 409 configure_push would change or remove a provider the manifest's push entry names. The detail names each field. Change the entry in the manifest and resubmit it with submit_manifest.
platform_minted_name 409 configure_push named a credential the platform minted, such as the platform credential. Store the provider credential under a name of your own and configure with that name.
push_not_configured 409 A send reached an environment with no provider configured. Configure a provider with configure_push.
realm_not_declared 404 A send reached an application whose manifest declares no accounts service, so no device is registered under a user. Add {"kind": "accounts"} to the manifest's services, resubmit it, and register devices after sign-in.
plan_quantity_unset 409 The plan's monthly push allowance is not set yet; nothing was sent. Send again once the platform sets the quantity; read_plan_quotas shows when it is set.
end_user_credential_not_admitted 403 A send ran under an end-user session; the platform refuses it before the route's own check. Send under the platform credential from the backend.
platform_credential_required 403 A send ran under a session or a minted token. Send under the platform credential from the backend.
usage_over_quota 429 The send would pass the plan's monthly push allowance. Wait for the month's first instant, or move to a larger plan.
invalid_request 400 A send's body or a registration's body has a malformed member, which the detail identifies, or a registration sending the session cookie came from another origin. A member containing a NUL character (U+0000) or an unpaired UTF-16 surrogate is refused too, and the detail identifies the character instead. Correct the named member, or register from the application's own origin or with the session as a bearer. Where the detail identifies the character, remove it.
session_required 401 A device registration or removal had no end-user session. Register after sign-in, with the session as the cookie or as a bearer.
invalid_client 400 A registration named a client_id the realm does not declare. Name the declared client the app signed in through, or no client.
not_found 404 A DELETE named an installation the signed-in user has not registered. Nothing to remove; the registration is already gone.