PushFlo

Go SDK

The official Go SDK is built for the processes that live below the browser: background workers, daemons, CLI tools, and job executors running at the OS level. Where the TypeScript SDK targets browsers and serverless functions, the Go SDK targets long-running programs that must stay connected for days — with auto-reconnect, heartbeat monitoring, and an optional end-to-end encrypted envelope baked in.

Installation

Requires Go 1.25 or newer. The SDK ships two packages: pushflo (subscriber client and REST publisher) and pushflo/envelope (transport-agnostic encryption layer).

Terminalbash
go get github.com/PushFlo/pushflo-go

Hands-on: a subscriber worker

The canonical Go SDK program is a worker that connects once and listens forever. Grab your publishable key from the console and you have a complete, production-shaped worker in about thirty lines:

main.gogo
package main

import (
	"context"
	"log"
	"os"
	"os/signal"
	"syscall"

	pushflo "github.com/PushFlo/pushflo-go"
)

func main() {
	ctx, stop := signal.NotifyContext(context.Background(),
		syscall.SIGINT, syscall.SIGTERM)
	defer stop()

	client, err := pushflo.NewClient(ctx, pushflo.ClientOptions{
		PublishKey: "pub_xxxxxxxxxxxxx",
	})
	if err != nil {
		log.Fatal(err)
	}
	defer client.Destroy()

	client.OnConnectionChange(func(s pushflo.ConnectionState) {
		log.Println("connection:", s)
	})

	if err := client.Connect(ctx); err != nil {
		log.Fatal(err)
	}

	sub, err := client.Subscribe("orders-user-123", pushflo.SubscriptionHandlers{
		OnMessage: func(m pushflo.Message) {
			log.Printf("[%s] %s", m.EventType, string(m.Content))
		},
	})
	if err != nil {
		log.Fatal(err)
	}
	defer sub.Unsubscribe()

	<-ctx.Done() // run until SIGINT/SIGTERM
}

That's the whole lifecycle: connect, subscribe, block until the OS asks you to stop. Reconnection, heartbeats, and backoff are handled for you — see the defaults below.

Hands-on: publishing

Publishing goes over REST with your secret key — same as the TypeScript server client, so Go workers and JS backends can publish to the same channels interchangeably.

publish.gogo
pub, err := pushflo.NewPublisher(pushflo.PublisherOptions{
	SecretKey: "sec_xxxxxxxxxxxxx",
})
if err != nil {
	log.Fatal(err)
}

err = pub.Publish(ctx, "orders", map[string]any{"status": "shipped"},
	pushflo.WithEventType("order.shipped"))

Connection resilience

Every option has a production-sensible default; a bare ClientOptions with just a key is a valid long-running configuration.

Auto-reconnect

  • AutoReconnect — on by default
  • MaxReconnectAttempts — 0 means retry forever
  • ReconnectDelay / MaxReconnectDelay — jittered exponential backoff

Dropped connections re-establish themselves with jittered exponential backoff, so a fleet of workers won't stampede the gateway after a network blip.

Heartbeat monitoring

  • HeartbeatInterval — configurable ping cadence
  • Two missed heartbeats — connection declared dead, reconnect begins

Half-open TCP connections — the classic silent failure mode for long-lived daemons — are detected and replaced instead of hanging forever.

Typed errors

  • ConnectionError
  • AuthenticationError
  • NetworkError
  • ValidationError

Errors are typed so your worker can decide programmatically: retry on NetworkError, page a human on AuthenticationError.

Encrypted envelope

Exclusive to the Go SDK: the envelope package treats PushFlo as untrusted transport. Payloads are sealed to the recipient's X25519 key and signed with the sender's Ed25519 key — the relay (us included) sees only ciphertext. Generate key material with go run ./examples/keygen.

The sender seals a payload and publishes it like any other message:

sender.gogo
import "github.com/PushFlo/pushflo-go/envelope"

sealer, err := envelope.NewSealer(envelope.SealerConfig{
	RecipientPublic: executorPub,   // X25519 — who may read it
	SignerPrivate:   cloudSignPriv, // Ed25519 — proves who sent it
	SignerKeyID:     "cloud-1",
})

// Seal and publish in one call:
err = pub.PublishSealed(ctx, "executor-1", sealer, job, envelope.Meta{})

The recipient opens it inside its message handler. A single OpenJSON call verifies the signature, checks freshness and replay, and decrypts:

recipient.gogo
opener, err := envelope.NewOpener(envelope.OpenerConfig{
	RecipientPublic:  executorPub,
	RecipientPrivate: executorPriv,
	Signers: map[string]ed25519.PublicKey{
		"cloud-1": cloudSignPub, // only these senders are trusted
	},
	MaxClockSkew: 2 * time.Minute,
	Replay:       envelope.NewMemoryReplayCache(10 * time.Minute),
})

OnMessage: func(m pushflo.Message) {
	env, err := envelope.Parse(m.Content)
	if err != nil {
		return
	}
	var job Job
	if err := opener.OpenJSON(env, &job); err != nil {
		// ErrBadSignature / ErrReplay / ErrExpired /
		// ErrDecrypt / ErrUnknownSigner
		log.Println("rejected envelope:", err)
		return
	}
	process(job)
},

Guarantees

  • Confidentiality — only the recipient's X25519 key decrypts
  • Authenticity & integrity — Ed25519 signature over all fields
  • Replay protection — message ID + timestamp with a replay cache

A captured envelope can't be read, forged, tampered with, or re-delivered later. Rejections come back as distinct sentinel errors so you can log exactly why a message was refused.

Reference executor & testing

The repository ships a reference worker at examples/executor — it receives sealed jobs, executes HTTP checks with per-phase timing (DNS, connect, TLS, TTFB), blocks egress to private addresses by default, and publishes sealed results back. It's the recommended starting point for building your own executor.

To run the SDK's integration tests against the live service with your own keys:

Terminalbash
PUSHFLO_PUBLISH_KEY=pub_... PUSHFLO_SECRET_KEY=sec_... \
  go test -run Integration -v .

The SDK is currently v0: the wire protocol is verified against the official TypeScript SDK v1.1.0 and the live service, but the public Go API may still evolve before v1. Source and issues live on GitHub.