PushFlo
Back to Blog
6 min readBy Marek

Introducing the Go SDK: End-to-End Encryption and Workers That Don't Fall Over

PushFlo now has an official Go SDK — built for OS-level workers and daemons, not browsers. It ships two things the JS SDK doesn't: an end-to-end encrypted envelope and connection handling designed to survive weeks of uptime.

productannouncementgosdksecurity

PushFlo has an official Go SDK: github.com/PushFlo/pushflo-go.

go get github.com/PushFlo/pushflo-go

Before you ask — no, it's not a port of the TypeScript SDK. It's a different tool for a different job, and that difference is the interesting part.

Different audience, different SDK

The TypeScript SDK lives where JavaScript lives: browser tabs, React components, serverless functions. Its world is short-lived — a tab that's open for twenty minutes, a Lambda that runs for two hundred milliseconds.

The Go SDK lives at the OS level. Its world is the background worker, the deploy agent, the monitoring daemon, the job executor — a process started by systemd or a container runtime that's expected to hold a connection for weeks. Nobody is watching it. Nobody presses F5 when it wedges.

That changes what the SDK has to be good at. A browser client that loses its connection gets rescued by the next page load. A daemon that loses its connection at 3am on a Saturday either rescues itself or delivers nothing until someone notices on Monday.

So we built the Go SDK around two things: connections that repair themselves, and a payload format that doesn't require trusting the transport — including us.

Feature one: the encrypted envelope

This is the headline, and it's exclusive to the Go SDK for now: the envelope package gives you true end-to-end encryption over PushFlo channels.

The design premise is blunt: the relay is treated as untrusted transport. Every payload is sealed to the recipient's X25519 public key and signed with the sender's Ed25519 key. What travels through PushFlo — and what sits in message history — is ciphertext. We can't read it. Neither can anyone who compromises a key with publish access to your channel.

Sealing on the sender side:

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

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

Opening on the recipient side:

opener, _ := envelope.NewOpener(envelope.OpenerConfig{
    RecipientPublic:  executorPub,
    RecipientPrivate: executorPriv,
    Signers:          map[string]ed25519.PublicKey{"cloud-1": cloudSignPub},
    MaxClockSkew:     2 * time.Minute,
    Replay:           envelope.NewMemoryReplayCache(10 * time.Minute),
})

var job Job
if err := opener.OpenJSON(env, &job); err != nil {
    // ErrBadSignature / ErrReplay / ErrExpired / ErrDecrypt / ErrUnknownSigner
    return
}

One OpenJSON call gets you three guarantees:

  1. Confidentiality — only the holder of the recipient's private key can decrypt.
  2. Authenticity and integrity — the Ed25519 signature covers every field. A tampered envelope fails, and an envelope from a signer you didn't list in Signers fails.
  3. Replay protection — message ID plus timestamp, checked against a replay cache. Capturing a valid "run this job" message and re-sending it an hour later gets you ErrReplay, not a second execution.

And when something is rejected, you get a sentinel error telling you whyErrBadSignature is a very different operational event than ErrExpired, and your logs will say which one happened.

Why does this matter for the worker use case specifically? Because the canonical Go SDK pattern is command and control: a cloud service dispatching jobs to executors running in environments you don't fully control — customer VPCs, edge boxes, CI runners. In that architecture "the messaging relay can read and forge jobs" is not an acceptable line in your threat model. With the envelope, PushFlo carries your messages the way the postal service carries a sealed letter: reliably, without reading it.

Feature two: rock-solid by default

The unglamorous half of the SDK is the part we spent the most time on. Long-running connection handling is a genre of code where everyone's first version is wrong, so the SDK's defaults are the production configuration:

  • Auto-reconnect with jittered exponential backoff. On by default, retries forever (MaxReconnectAttempts: 0), with jitter so a fleet of a hundred workers doesn't reconnect-stampede the gateway after a network blip. Subscriptions are re-established automatically after reconnect.
  • Heartbeat monitoring. The failure mode that actually kills daemons isn't the dropped connection — it's the half-open one, where TCP still looks alive but nothing flows. The client pings on a fixed interval, and two silent heartbeats means the connection is declared dead and rebuilt. No hanging forever on a socket that will never speak again.
  • Typed errors. ConnectionError, AuthenticationError, NetworkError, ValidationError — so your process can make real decisions: retry on NetworkError, exit loudly on AuthenticationError instead of retrying a key that will never work.
  • Context-first API. Every blocking call takes a context.Context. Wire it to signal.NotifyContext and your worker shuts down cleanly on SIGTERM, the way an OS-level process should.

A complete production-shaped worker is about thirty lines — connect, subscribe, block on ctx.Done(). The full walkthrough is in the Go SDK docs.

We also want to be precise about maturity, because "rock solid" earns skepticism from a v0: the wire protocol is verified against the official TypeScript SDK v1.1.0 and against the live service in integration tests. What may still shift before v1 is the Go API surface — names and signatures, not behavior on the wire.

A reference executor, included

The repo ships a working reference at examples/executor — an HTTP-check worker that receives sealed jobs, executes them with per-phase timing (DNS, connect, TLS, time-to-first-byte), refuses to talk to private IP ranges by default, and publishes sealed results back. There's also examples/keygen for generating the X25519/Ed25519 key material, so nobody has to hand-roll key generation.

If you're building anything shaped like "cloud dispatches work, agents execute it," that example is the intended starting point.

Which SDK do I use?

Simple rule:

You're buildingUse
Browser UI, React app, notifications in a web pageTypeScript SDK (npm)
Serverless function that publishesEither — both publish over REST
Worker, daemon, CLI, agent, executorGo SDK
Anything where the relay must not read payloadsGo SDK with the envelope

Same channels, same wire protocol, same pricing — a Go worker and a Next.js frontend can talk to each other through one PushFlo project today. Docs are at /docs/go-sdk, source is on GitHub, and if you hit anything rough in the v0, an issue on the repo goes straight to the person who wrote it.

Ship a worker that stays up

Grab your free API keys, go get the SDK, and have a self-healing, end-to-end encrypted Go worker running before your coffee cools.

M

Written by Marek

Indie developer building PushFlo — real-time messaging for serverless apps. Every post comes from running the thing in production. More about PushFlo →