{"id":50336841,"url":"https://github.com/localytics/push-notification-protos","last_synced_at":"2026-05-29T14:01:19.344Z","repository":{"id":357179073,"uuid":"1235702960","full_name":"localytics/push-notification-protos","owner":"localytics","description":"Protocol Buffer definitions for the push notification service.","archived":false,"fork":false,"pushed_at":"2026-05-11T17:35:32.000Z","size":14,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-11T18:33:13.701Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/localytics.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-11T15:16:21.000Z","updated_at":"2026-05-11T17:35:38.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/localytics/push-notification-protos","commit_stats":null,"previous_names":["localytics/push-notification-protos"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/localytics/push-notification-protos","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/localytics%2Fpush-notification-protos","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/localytics%2Fpush-notification-protos/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/localytics%2Fpush-notification-protos/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/localytics%2Fpush-notification-protos/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/localytics","download_url":"https://codeload.github.com/localytics/push-notification-protos/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/localytics%2Fpush-notification-protos/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33655441,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-29T02:00:06.066Z","response_time":107,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-05-29T14:01:18.319Z","updated_at":"2026-05-29T14:01:19.308Z","avatar_url":"https://github.com/localytics.png","language":null,"funding_links":[],"categories":[],"sub_categories":[],"readme":"# push-notification-protos\n\nProtocol Buffer definitions for the Localytics push notification service.\n\nThis repository contains a single source-of-truth schema, [`push.proto`](push.proto), that defines a bidirectional streaming gRPC API for sending push notifications to iOS, Android, and Web clients. The long-form client guide — with full runnable examples in Go, Python, Node.js, Java, and `grpcurl`, plus the full table of gRPC status codes — lives in [`usage.md`](usage.md). This README is the entry point: it explains what is in the proto, how the protocol works, and how to wire the generated stubs into a codebase.\n\n## Table of contents\n\n- [What `push.proto` defines](#what-pushproto-defines)\n- [Message reference](#message-reference)\n- [The streaming protocol](#the-streaming-protocol)\n- [Authentication and endpoint](#authentication-and-endpoint)\n- [Limits worth knowing up front](#limits-worth-knowing-up-front)\n- [Integrating into your codebase](#integrating-into-your-codebase)\n  - [1. Vendor the proto](#1-vendor-the-proto)\n  - [2. Generate stubs](#2-generate-stubs)\n  - [3. Wire up a client](#3-wire-up-a-client)\n- [Versioning and compatibility](#versioning-and-compatibility)\n- [Repo layout](#repo-layout)\n\n## What `push.proto` defines\n\nThe file is plain `proto3`. It declares:\n\n- The package `push` (so generated symbols live under `push.*`, `push_pb2`, etc.).\n- A Go import path: `option go_package = \"github.com/localytics/push-notification-protos/push\"`. Generated Go code will live under this import path.\n- A single external import, `google/protobuf/struct.proto`, which ships with `protoc` and the language plugins. It is only used to type the free-form `extra` fields on the platform params messages.\n- One gRPC service, `PushService`, with one RPC, `StreamPush`, which is **bidirectional streaming**.\n- A set of request and response messages used to drive that stream.\n\nThere are no enums and no nested messages; every type is top-level. Field numbers are stable — only additive changes should be expected.\n\n## Message reference\n\nThe proto groups messages into a few small layers. Every field listed here comes from `push.proto` directly.\n\n### `Alert` — the notification content\n\nThe user-visible part of the push.\n\n- `body` (`string`, required) — the notification text.\n- `title` (`string`, optional) — short title.\n- `subtitle` (`string`, optional) — iOS 10+ only; the server rejects `subtitle` if `title` is not also set.\n\n### Per-platform parameter messages\n\nEach is optional. Set the ones you need; omitted platforms are simply not delivered to.\n\n**`IOSParams`** — iOS-specific options:\n\n- `sound` (`string`) — sound file name, or `\"default\"`.\n- `badge` (`int32`) — unread badge number.\n- `category` (`string`) — interactive push category.\n- `content_available` (`bool`) — defaults to `true`.\n- `mutable_content` (`bool`) — defaults to `false`.\n- `extra` (`google.protobuf.Struct`) — arbitrary JSON-shaped payload merged into the APNs message.\n\n**`AndroidParams`** — Android-specific options:\n\n- `priority` (`string`) — message priority, typically `\"normal\"` or `\"high\"`.\n- `extra` (`google.protobuf.Struct`) — arbitrary JSON-shaped payload.\n\n**`WebParams`** — Web push options:\n\n- `badge` (`string`) — URL to a badge image.\n- `dir` (`string`) — `\"ltr\"`, `\"rtl\"`, or `\"auto\"`.\n- `icon` (`string`) — URL to an icon image.\n- `require_interaction` (`bool`)\n- `renotify` (`bool`)\n- `silent` (`bool`)\n- `tag` (`string`)\n- `extra` (`google.protobuf.Struct`)\n\nThe `extra` fields are typed as `google.protobuf.Struct`, which represents an arbitrary JSON object (nested objects and arrays allowed, not just flat string maps). Every language has a builder for it: `structpb.NewStruct(...)` in Go, `struct_pb2.Struct().update({...})` in Python, `Struct.newBuilder().putFields(...)` in Java, and a plain JS object in Node.js.\n\n### `Labels` — up to 10 tracking labels\n\nA flat container with fields `label1` through `label10`, all `string` and all optional. Used for campaign performance tracking; the meaning of each slot is yours to decide.\n\n### `StreamInit` — the first message on the stream\n\nSent once, as the first frame, to identify the stream.\n\n- `app_id` (`string`, required) — the application UUID.\n- `request_id` (`string`, optional, ≤ 255 chars) — auto-generated by the server if omitted.\n- `campaign_key` (`string`, optional, ≤ 255 chars, must match `^[\\w\\-.]+$`).\n- `labels` (`Labels`, optional).\n- `all_devices` (`bool`, optional) — defaults to `false`.\n- `test` (`bool`, optional) — test-mode flag; messages are validated but not delivered.\n\n### `PushRequest` — one per batch of recipients\n\nSent zero or more times after `StreamInit`. Each `PushRequest` targets one or more `customer_ids` with the same alert and platform params.\n\n- `customer_ids` (`repeated string`, required, non-empty) — up to 30,000 per request.\n- `alert` (`Alert`, required) — rejected if missing.\n- `ios` (`IOSParams`, optional).\n- `android` (`AndroidParams`, optional).\n- `web` (`WebParams`, optional).\n\n### `PushStreamMessage` — client-to-server envelope\n\nA `oneof` wrapper so the stream can carry either kind of frame:\n\n```protobuf\nmessage PushStreamMessage {\n  oneof payload {\n    StreamInit init = 1;\n    PushRequest push = 2;\n  }\n}\n```\n\nThe first frame must populate `init`; every subsequent frame must populate `push`. This is enforced by the server, not by the proto.\n\n### `PushFailure` — partial failure (server → client)\n\nStreamed back zero or more times as failures are discovered. The stream stays open; it does **not** terminate the call.\n\n- `customer_ids` (`repeated string`) — the subset of IDs that failed (from a `PushRequest` you sent).\n- `reason` (`string`) — human-readable failure description.\n\n### `PushResponse` — final summary (server → client)\n\nSent exactly once, always last, just before the server closes the stream.\n\n- `request_id` (`string`) — matches the stream's request ID (the one you sent in `StreamInit`, or the one the server generated).\n- `total_messages` (`int32`) — number of `PushRequest` messages processed.\n- `total_customer_ids` (`int32`) — total IDs across all messages.\n- `status` (`string`) — `\"accepted\"` or `\"error\"`.\n- `error` (`string`, optional) — set only when `status == \"error\"`.\n- `campaign_id` (`int64`) — server-assigned campaign ID.\n\n### `PushStreamResponse` — server-to-client envelope\n\nMirrors `PushStreamMessage` on the server side:\n\n```protobuf\nmessage PushStreamResponse {\n  oneof response {\n    PushFailure failure = 1;\n    PushResponse summary = 2;\n  }\n}\n```\n\n### `PushService` — the RPC\n\n```protobuf\nservice PushService {\n  rpc StreamPush(stream PushStreamMessage) returns (stream PushStreamResponse);\n}\n```\n\nOne method, both sides streaming. There are no other RPCs.\n\n## The streaming protocol\n\nThe proto defines the *shape* of the messages; the *order* in which they're allowed is part of the contract. Every client must do this:\n\n```\nclient → StreamInit                       (must be first; exactly one)\nclient → PushRequest                      (zero or more; in any order)\nclient → PushRequest\nclient → ... CloseSend / half-close ...   (signal you're done sending)\n\nserver → PushFailure                      (zero or more, as failures happen)\nserver → PushFailure\nserver → PushResponse summary             (exactly one; always last)\nserver → EOF                              (server closes the stream)\n```\n\nConcretely:\n\n1. Open the bidirectional stream by calling `StreamPush` on the generated client.\n2. Send a single `PushStreamMessage` with the `init` arm populated. The server rejects any other frame in this slot.\n3. Send as many `PushStreamMessage`s with the `push` arm populated as you need, batching `customer_ids` per request to stay efficient.\n4. Half-close the send side (`CloseSend()` in Go, `stream.end()` in Node, `done_writing()` in Python aio, `requestObserver.onCompleted()` in Java). Until you do this, the server will not emit its summary.\n5. Read every response until EOF. Treat `PushFailure` frames as partial-success information — they do **not** end the stream. The `PushResponse` summary is always the last message before EOF.\n\nA common bug is to stop reading after the first `PushFailure`. Don't — the summary still has to come through.\n\n## Authentication and endpoint\n\nThe service is reachable on the sandbox at:\n\n```\ntrans-api-grpc.sandbox53.localytics.com:50051\n```\n\nAuthentication is HTTP Basic, carried in gRPC metadata on the call (not in the proto itself):\n\n```\nauthorization: Basic \u003cbase64(api_key:api_secret)\u003e\n```\n\nThe server authenticates the call, then verifies that `StreamInit.app_id` belongs to the authenticated organization. See [`usage.md`](usage.md) for the full mapping from auth failures to gRPC status codes.\n\n## Limits worth knowing up front\n\nThese limits are enforced by the server, not encoded in the proto, but they shape how you should batch:\n\n- **10,000** `PushRequest` messages per stream.\n- **30,000** `customer_ids` per `PushRequest`.\n- **10 minutes** of wall-clock per stream.\n- **4 MiB** maximum message size.\n- `request_id` and `campaign_key` ≤ **255 chars**; `campaign_key` must match `^[\\w\\-.]+$`.\n\nIf you have more recipients than fit in one stream, open multiple streams in parallel — each gets its own `request_id` and limits independently.\n\n## Integrating into your codebase\n\nThe mechanics are the same in every language: vendor the proto, generate stubs, then write a small client that drives the protocol described above.\n\n### 1. Vendor the proto\n\nYou have three reasonable options:\n\n- **Git submodule.** `git submodule add https://.../push-notification-protos third_party/push-notification-protos` and point your build at `third_party/push-notification-protos/push.proto`. Easiest way to track upstream and stay pinned to a specific commit.\n- **Pin a tag and copy.** Copy `push.proto` (and `LICENSE`) into your repo under e.g. `proto/push.proto`. Record the upstream commit hash somewhere in your repo so you can re-sync intentionally.\n- **`buf` modules.** If you use [Buf](https://buf.build/), you can publish this proto as a module and depend on it from your `buf.yaml`.\n\n`push.proto` only imports `google/protobuf/struct.proto`. That import ships with `protoc` itself (under the `include/` directory of the protobuf release) and with each language's protobuf runtime, so you do **not** need to vendor it separately.\n\n### 2. Generate stubs\n\nUse `protoc` (or `buf generate`). Example invocations are below; replace `proto/push.proto` with wherever you placed the file.\n\n**Go.** The generated package import path follows the `option go_package` in the proto, i.e. `github.com/localytics/push-notification-protos/push`.\n\n```bash\ngo install google.golang.org/protobuf/cmd/protoc-gen-go@latest\ngo install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest\n\nprotoc \\\n  --go_out=. --go_opt=paths=source_relative \\\n  --go-grpc_out=. --go-grpc_opt=paths=source_relative \\\n  proto/push.proto\n\ngo get google.golang.org/grpc google.golang.org/protobuf\n```\n\nThis produces `push.pb.go` (message types) and `push_grpc.pb.go` (the `PushServiceClient` interface and `NewPushServiceClient` constructor).\n\n**Python.** Use `grpcio-tools` so the gRPC stubs are generated alongside the message types.\n\n```bash\npip install grpcio grpcio-tools protobuf\n\npython -m grpc_tools.protoc \\\n  -I proto \\\n  --python_out=./gen \\\n  --grpc_python_out=./gen \\\n  proto/push.proto\n```\n\nThis produces `push_pb2.py` (messages) and `push_pb2_grpc.py` (the `PushServiceStub`).\n\n**Node.js / TypeScript.** Two common approaches:\n\n- *Dynamic loading*, no codegen step: `npm install @grpc/grpc-js @grpc/proto-loader`, then `protoLoader.loadSync('push.proto', ...)` at runtime. Simplest to get started; no TypeScript types out of the box.\n- *Static codegen*: `npm install -D grpc-tools grpc_tools_node_protoc_ts`, run `grpc_tools_node_protoc` against `push.proto`. You get `.js` + `.d.ts` and full type checking.\n\n**Java.** Either invoke `protoc` directly:\n\n```bash\nprotoc --java_out=src/main/java --grpc-java_out=src/main/java proto/push.proto\n```\n\n…or, more commonly, use the `com.google.protobuf` Gradle/Maven plugin pointed at your proto directory. Add these runtime dependencies:\n\n```groovy\nimplementation 'io.grpc:grpc-netty-shaded:1.68.0'\nimplementation 'io.grpc:grpc-protobuf:1.68.0'\nimplementation 'io.grpc:grpc-stub:1.68.0'\nimplementation 'com.google.protobuf:protobuf-java:4.29.0'\n```\n\nThe generated classes live under the `push` package (from the proto's `package push;` declaration).\n\n**Other languages.** `push.proto` is plain proto3 with one well-known import, so it works unchanged with `protoc` plugins for C#, Ruby, Kotlin, Swift, and Rust (`tonic`). Generate stubs the same way and follow the protocol described above.\n\n### 3. Wire up a client\n\nThe same five steps in every language:\n\n1. **Open a channel/connection** to the endpoint. Reuse one channel across the application — gRPC multiplexes streams over a single HTTP/2 connection, so you don't need a new channel per call.\n2. **Attach auth metadata.** Build the `Basic` header from `api_key:api_secret`, base64-encode it, and attach it to the call (per-RPC metadata is fine; you don't have to use an interceptor).\n3. **Set a deadline.** The server enforces a 10-minute cap; set your client deadline slightly above that (e.g. 11 minutes) as a safety net.\n4. **Drive the stream.** Send one `PushStreamMessage{ init: StreamInit{...} }`, then any number of `PushStreamMessage{ push: PushRequest{...} }`, then half-close.\n5. **Drain responses until EOF.** Switch on the `response` oneof: log/collect `PushFailure`s, store the final `PushResponse` summary, and break on EOF (or `onCompleted`, depending on language).\n\nSelf-contained, minimal example clients live under [`examples/`](examples):\n\n- [`examples/go/`](examples/go) — Go + `protoc`, driven by a `Makefile`.\n- [`examples/java/`](examples/java) — Java + Gradle with the `protobuf-gradle-plugin`.\n- [`examples/rust/`](examples/rust) — Rust + `tonic` (codegen via `build.rs`).\n\nAll three are roughly 100 lines each and exercise the full flow: open the stream, send `StreamInit` + one `PushRequest`, half-close, drain failures, print the summary.\n\nFull client examples for Python (sync and async), Node.js, and `grpcurl`, plus the complete gRPC status-code table, retry strategy, and the `google.protobuf.Struct` builders for the `extra` fields in each language, live in [`usage.md`](usage.md).\n\n## Versioning and compatibility\n\nTreat `push.proto` as a public contract:\n\n- Field numbers are stable. Don't reuse or renumber them locally.\n- Only additive changes (new optional fields, new `oneof` arms with new tags) should be expected on existing messages. Wire compatibility goes both directions for these.\n- Pin to a specific git tag or commit when vendoring, so a regen never silently picks up an upstream change.\n\n## Repo layout\n\n```\npush.proto    # the schema — this is what you generate stubs from\nusage.md      # full client integration guide (auth, limits, runnable examples)\nexamples/     # minimal self-contained clients (Go, Java, Rust)\nLICENSE\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flocalytics%2Fpush-notification-protos","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flocalytics%2Fpush-notification-protos","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flocalytics%2Fpush-notification-protos/lists"}