Skip to content

localytics/push-notification-protos

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

push-notification-protos

Protocol Buffer definitions for the Localytics push notification service.

This repository contains a single source-of-truth schema, 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. 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.

Table of contents

What push.proto defines

The file is plain proto3. It declares:

  • The package push (so generated symbols live under push.*, push_pb2, etc.).
  • A Go import path: option go_package = "github.com/localytics/push-notification-protos/push". Generated Go code will live under this import path.
  • 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.
  • One gRPC service, PushService, with one RPC, StreamPush, which is bidirectional streaming.
  • A set of request and response messages used to drive that stream.

There are no enums and no nested messages; every type is top-level. Field numbers are stable — only additive changes should be expected.

Message reference

The proto groups messages into a few small layers. Every field listed here comes from push.proto directly.

Alert — the notification content

The user-visible part of the push.

  • body (string, required) — the notification text.
  • title (string, optional) — short title.
  • subtitle (string, optional) — iOS 10+ only; the server rejects subtitle if title is not also set.

Per-platform parameter messages

Each is optional. Set the ones you need; omitted platforms are simply not delivered to.

IOSParams — iOS-specific options:

  • sound (string) — sound file name, or "default".
  • badge (int32) — unread badge number.
  • category (string) — interactive push category.
  • content_available (bool) — defaults to true.
  • mutable_content (bool) — defaults to false.
  • extra (google.protobuf.Struct) — arbitrary JSON-shaped payload merged into the APNs message.

AndroidParams — Android-specific options:

  • priority (string) — message priority, typically "normal" or "high".
  • extra (google.protobuf.Struct) — arbitrary JSON-shaped payload.

WebParams — Web push options:

  • badge (string) — URL to a badge image.
  • dir (string) — "ltr", "rtl", or "auto".
  • icon (string) — URL to an icon image.
  • require_interaction (bool)
  • renotify (bool)
  • silent (bool)
  • tag (string)
  • extra (google.protobuf.Struct)

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

Labels — up to 10 tracking labels

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

StreamInit — the first message on the stream

Sent once, as the first frame, to identify the stream.

  • app_id (string, required) — the application UUID.
  • request_id (string, optional, ≤ 255 chars) — auto-generated by the server if omitted.
  • campaign_key (string, optional, ≤ 255 chars, must match ^[\w\-.]+$).
  • labels (Labels, optional).
  • all_devices (bool, optional) — defaults to false.
  • test (bool, optional) — test-mode flag; messages are validated but not delivered.

PushRequest — one per batch of recipients

Sent zero or more times after StreamInit. Each PushRequest targets one or more customer_ids with the same alert and platform params.

  • customer_ids (repeated string, required, non-empty) — up to 30,000 per request.
  • alert (Alert, required) — rejected if missing.
  • ios (IOSParams, optional).
  • android (AndroidParams, optional).
  • web (WebParams, optional).

PushStreamMessage — client-to-server envelope

A oneof wrapper so the stream can carry either kind of frame:

message PushStreamMessage {
  oneof payload {
    StreamInit init = 1;
    PushRequest push = 2;
  }
}

The first frame must populate init; every subsequent frame must populate push. This is enforced by the server, not by the proto.

PushFailure — partial failure (server → client)

Streamed back zero or more times as failures are discovered. The stream stays open; it does not terminate the call.

  • customer_ids (repeated string) — the subset of IDs that failed (from a PushRequest you sent).
  • reason (string) — human-readable failure description.

PushResponse — final summary (server → client)

Sent exactly once, always last, just before the server closes the stream.

  • request_id (string) — matches the stream's request ID (the one you sent in StreamInit, or the one the server generated).
  • total_messages (int32) — number of PushRequest messages processed.
  • total_customer_ids (int32) — total IDs across all messages.
  • status (string) — "accepted" or "error".
  • error (string, optional) — set only when status == "error".
  • campaign_id (int64) — server-assigned campaign ID.

PushStreamResponse — server-to-client envelope

Mirrors PushStreamMessage on the server side:

message PushStreamResponse {
  oneof response {
    PushFailure failure = 1;
    PushResponse summary = 2;
  }
}

PushService — the RPC

service PushService {
  rpc StreamPush(stream PushStreamMessage) returns (stream PushStreamResponse);
}

One method, both sides streaming. There are no other RPCs.

The streaming protocol

The proto defines the shape of the messages; the order in which they're allowed is part of the contract. Every client must do this:

client → StreamInit                       (must be first; exactly one)
client → PushRequest                      (zero or more; in any order)
client → PushRequest
client → ... CloseSend / half-close ...   (signal you're done sending)

server → PushFailure                      (zero or more, as failures happen)
server → PushFailure
server → PushResponse summary             (exactly one; always last)
server → EOF                              (server closes the stream)

Concretely:

  1. Open the bidirectional stream by calling StreamPush on the generated client.
  2. Send a single PushStreamMessage with the init arm populated. The server rejects any other frame in this slot.
  3. Send as many PushStreamMessages with the push arm populated as you need, batching customer_ids per request to stay efficient.
  4. 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.
  5. 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.

A common bug is to stop reading after the first PushFailure. Don't — the summary still has to come through.

Authentication and endpoint

The service is reachable on the sandbox at:

trans-api-grpc.sandbox53.localytics.com:50051

Authentication is HTTP Basic, carried in gRPC metadata on the call (not in the proto itself):

authorization: Basic <base64(api_key:api_secret)>

The server authenticates the call, then verifies that StreamInit.app_id belongs to the authenticated organization. See usage.md for the full mapping from auth failures to gRPC status codes.

Limits worth knowing up front

These limits are enforced by the server, not encoded in the proto, but they shape how you should batch:

  • 10,000 PushRequest messages per stream.
  • 30,000 customer_ids per PushRequest.
  • 10 minutes of wall-clock per stream.
  • 4 MiB maximum message size.
  • request_id and campaign_key255 chars; campaign_key must match ^[\w\-.]+$.

If you have more recipients than fit in one stream, open multiple streams in parallel — each gets its own request_id and limits independently.

Integrating into your codebase

The mechanics are the same in every language: vendor the proto, generate stubs, then write a small client that drives the protocol described above.

1. Vendor the proto

You have three reasonable options:

  • 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.
  • 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.
  • buf modules. If you use Buf, you can publish this proto as a module and depend on it from your buf.yaml.

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.

2. Generate stubs

Use protoc (or buf generate). Example invocations are below; replace proto/push.proto with wherever you placed the file.

Go. The generated package import path follows the option go_package in the proto, i.e. github.com/localytics/push-notification-protos/push.

go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

protoc \
  --go_out=. --go_opt=paths=source_relative \
  --go-grpc_out=. --go-grpc_opt=paths=source_relative \
  proto/push.proto

go get google.golang.org/grpc google.golang.org/protobuf

This produces push.pb.go (message types) and push_grpc.pb.go (the PushServiceClient interface and NewPushServiceClient constructor).

Python. Use grpcio-tools so the gRPC stubs are generated alongside the message types.

pip install grpcio grpcio-tools protobuf

python -m grpc_tools.protoc \
  -I proto \
  --python_out=./gen \
  --grpc_python_out=./gen \
  proto/push.proto

This produces push_pb2.py (messages) and push_pb2_grpc.py (the PushServiceStub).

Node.js / TypeScript. Two common approaches:

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

Java. Either invoke protoc directly:

protoc --java_out=src/main/java --grpc-java_out=src/main/java proto/push.proto

…or, more commonly, use the com.google.protobuf Gradle/Maven plugin pointed at your proto directory. Add these runtime dependencies:

implementation 'io.grpc:grpc-netty-shaded:1.68.0'
implementation 'io.grpc:grpc-protobuf:1.68.0'
implementation 'io.grpc:grpc-stub:1.68.0'
implementation 'com.google.protobuf:protobuf-java:4.29.0'

The generated classes live under the push package (from the proto's package push; declaration).

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.

3. Wire up a client

The same five steps in every language:

  1. 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.
  2. 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).
  3. 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.
  4. Drive the stream. Send one PushStreamMessage{ init: StreamInit{...} }, then any number of PushStreamMessage{ push: PushRequest{...} }, then half-close.
  5. Drain responses until EOF. Switch on the response oneof: log/collect PushFailures, store the final PushResponse summary, and break on EOF (or onCompleted, depending on language).

Self-contained, minimal example clients live under examples/:

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

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

Versioning and compatibility

Treat push.proto as a public contract:

  • Field numbers are stable. Don't reuse or renumber them locally.
  • 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.
  • Pin to a specific git tag or commit when vendoring, so a regen never silently picks up an upstream change.

Repo layout

push.proto    # the schema — this is what you generate stubs from
usage.md      # full client integration guide (auth, limits, runnable examples)
examples/     # minimal self-contained clients (Go, Java, Rust)
LICENSE

About

Protocol Buffer definitions for the push notification service.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors