---
source_url: https://www.pubnub.com/docs/sdks/c/api-reference/providers
title: Providers API for C SDK
updated_at: 2026-09-24T12:25:41.000Z
sdk_name: PubNub C SDK
sdk_version: 1.0.0
---

# Providers API for C SDK

PubNub C SDK, use the latest version: 1.0.0

## Documentation index

To discover more PubNub resources:

1. Fetch [PubNub's llms.txt](https://www.pubnub.com/llms-full.txt) for a list of available pages in Markdown format.
2. Identify relevant URLs from that index.
3. Fetch the target pages.

Do not assume a path exists, always check the index first.

Six independently swappable **provider** vtables sit underneath every PubNub context: allocator, transport, serialization, platform, crypto, and logger. Each is a function-pointer struct declared in a header under `#include <pubnub/providers/...>`. Select a backend at CMake configure time with `PUBNUB_PROVIDER_<FAMILY>`. Or override a single family per context: point the matching `pubnub_config_t` field at a vtable instance you implement yourself. This is how you port the SDK to a platform it does not ship a backend for, or swap in your own HTTP stack, JSON parser, allocator, or crypto library.

This page documents the six interface headers: exact member prototypes, which members are mandatory, how the SDK validates and sequences them, and the patterns for implementing your own. Three closely related topics live elsewhere, and this page cross-links to them instead of repeating them:

* The `pubnub_config_t` provider pointer **fields** (types, defaults, the borrowed-always rule) are in [Configuration](https://www.pubnub.com/docs/sdks/c/api-reference/configuration.md#providers).
* The CMake `PUBNUB_PROVIDER_<FAMILY>` backend-selection strings and per-profile defaults are in [Environment Setup](https://www.pubnub.com/docs/sdks/c/environment-setup.md#provider-backends).
* The logger vtable's consumer-facing behavior (log levels, structured values, registration API, fan-out order) is in [Logging](https://www.pubnub.com/docs/sdks/c/logging.md).
* The crypto module API (`pubnub_crypto_module_t`, the two built-in algorithms, migration between them) is in [Encryption](https://www.pubnub.com/docs/sdks/c/api-reference/encryption.md).

TLS is **not** a seventh provider family. It's controlled by the `PUBNUB_ENABLE_SECURE_TRANSPORT` compile-time toggle (ON by default). When it's on, the transport provider (`curl` or `socket`) negotiates TLS internally; there is no separate TLS vtable to implement or swap. Turning it off excludes all TLS code from the binary, for a target that terminates TLS externally or that intentionally talks plaintext.

:::warning Getting a vtable member's signature wrong is undefined behavior, not a compile error
Assigning a function to the wrong vtable field often compiles with at most a warning, then corrupts the stack the first time the SDK calls through it. Copy the prototypes on this page exactly, including parameter order and pointer levels, and never infer a signature from a field name or from another family's shape.
:::

## How provider resolution and validation works

Every one of the six `pubnub_config_t` provider fields is a pointer. Leave a field `NULL` and the compiled-in default for that family is used. Supply a non-`NULL` pointer and the SDK validates it before use.

Four families are validated for a fixed set of mandatory members, and a missing one is rejected outright:

| Family | Mandatory members | Missing-member result |
| --- | --- | --- |
| Allocator | `alloc`, `free`, `buf_acquire`, `buf_release` | `PUBNUB_ERR_PROVIDER_MISSING` |
| Transport | `send`, `poll`, `cancel` | `PUBNUB_ERR_PROVIDER_MISSING` |
| Serialization | `parse`, `serialize`, `value_destroy` | `PUBNUB_ERR_PROVIDER_MISSING` |
| Platform | `monotonic_ms`, `wall_clock_ms`, `sleep_ms`, `random_bytes` | `PUBNUB_ERR_PROVIDER_MISSING` (see note below) |

:::warning Crypto and logger have no missing-member contract
The raw crypto provider vtable and the logger vtable are **not** validated like the four families above. `encrypt`, `decrypt`, and `hmac_sha256` are each independently optional on a crypto provider. `pubnub_init`/`pubnub_create` accept one with any of the three left `NULL`, because crypto operations are opt-in per capability.
A provider that only signs Access Manager requests can omit `encrypt`/`decrypt`. One that only encrypts can omit `hmac_sha256`. No missing member on a crypto provider is rejected, and the logger vtable is accepted the same way, including a `NULL` logger provider itself. Do not assume `PUBNUB_ERR_PROVIDER_MISSING` applies to either family.
:::

:::note wall_clock_ms missing vs. wall_clock_ms returning 0 are two different failures
`wall_clock_ms` being absent (`NULL`) from the platform vtable is caught at the same time and the same way as the other three mandatory platform members: `pubnub_create()`/`pubnub_init()` fails with `PUBNUB_ERR_PROVIDER_MISSING`, before any request is ever sent. That is a different failure from a *present* `wall_clock_ms` returning `0` at call time (no RTC, not yet NTP-synchronized) — context creation succeeds in that case, and the failure only surfaces later, at the moment a PAM-signed request tries to sign itself, as `PUBNUB_ERR_NO_WALL_CLOCK`. See [Platform](#platform) below and [Troubleshooting — PAM](https://www.pubnub.com/docs/sdks/c/troubleshooting.md#pam--pubnub_err_no_wall_clock) for both cases.
:::

### Resolution and lifecycle order

Providers are resolved and initialized inline, synchronously, during `pubnub_create()`/`pubnub_init()`, in this order:

1. The **allocator** is resolved first (your supplied pointer, or the compiled-in default), then the **platform** provider is resolved.
2. If the allocator implements `init`, it runs immediately, before anything else, because the allocator must be usable before the `pubnub_provider_deps_t` bundle exists (see [The provider dependency bundle](#the-provider-dependency-bundle) below, and the allocator's own lifecycle note in [Allocator](#allocator)).
3. **Transport** and **serialization** are resolved to your supplied pointer or their compiled-in default. `crypto_module` is stored as a bare pointer at this stage, with no lifecycle call.
4. The four mandatory-member checks above run, in the order allocator, transport, serialization, platform. If validation fails here, only the allocator (if its `init` already ran in step 2) is rolled back.
5. The `pubnub_provider_deps_t` bundle is populated.
6. `transport->init(transport, &deps)` runs, then `serialization->init(serialization, &deps)` runs. Transport initializes before serialization. If `PUBNUB_ENABLE_CRYPTO` is on and `crypto_module` is non-`NULL`, the module then calls `init` on its default cryptor and every fallback cryptor, last.
7. On teardown (`pubnub_destroy()`/`pubnub_deinit()`), providers deinitialize in the **reverse** of their init order: the crypto module's cryptors first (if attached), then `serialization->deinit()`, then `transport->deinit()`, then the allocator's `deinit`.

If any `init` call in this sequence fails, the SDK rolls back only the providers whose `init` already succeeded, calling their `deinit` in reverse, and returns `PUBNUB_ERR_INTERNAL`. A provider whose `init` never ran is never handed a `deinit` call during rollback.

### Threading and re-entrancy

Every provider header states the same rule, in near-identical wording:

* Allocator, transport, and serialization callbacks run "from non-ISR context only."
* Platform and crypto callbacks are "invoked from normal (non-ISR) context only."
* Logger callbacks are "never called from ISR context."

None of the six vtables may be invoked from an interrupt handler on an embedded target.

No provider header says whether it is safe to call back into the SDK (for example, calling a feature function on the same context) from inside one of your own provider callbacks. Treat this as unverified, not merely undocumented, and avoid it.

### Ownership

Every one of the six provider pointer fields on `pubnub_config_t` is **always borrowed**, whether the context was created with `pubnub_create()` or `pubnub_init()`. This is the one exception to the general string-ownership split those two functions otherwise apply. You must keep every provider struct you supply alive for as long as any context references it. See [Configuration](https://www.pubnub.com/docs/sdks/c/api-reference/configuration.md#providers) for the full ownership discussion at the config-field level.

## The provider dependency bundle

Transport's and serialization's optional `init` callbacks receive a `pubnub_provider_deps_t` bundle, giving them access to the other shared infrastructure providers without a direct compile-time dependency:

```c
typedef struct pubnub_provider_deps {
    struct pubnub_allocator_provider* allocator;
    struct pubnub_logger_provider*    logger;
    struct pubnub_platform_provider*  platform;
    const struct pubnub_proxy_config* proxy;
    const struct pubnub_tcp_keepalive_config* tcp_keepalive;
} pubnub_provider_deps_t;
```

| Parameter | Description |
| --- | --- |
| `allocator` | Always non-`NULL`. |
| `logger` | May be `NULL` if logging is disabled for the context. |
| `platform` | Always non-`NULL`. |
| `proxy` | Points into the context's own config storage. `NULL` exactly when `pubnub_config_t.proxy.type` is `PUBNUB_PROXY_NONE`. |
| `tcp_keepalive` | Points into the context's own config storage. `NULL` exactly when `pubnub_config_t.tcp_keepalive.enabled` is `0`. |

Transport and serialization receive this bundle through a real `init(self, &deps)` call. A crypto provider's `init(self, const pubnub_provider_deps_t*)` receives the same bundle, but only when the provider is wrapped in a `pubnub_crypto_module_t` and attached to the context. See [Crypto (provider)](#crypto-provider) below for how that call is sequenced. The allocator does **not** receive this bundle at all, for a reason covered next. Platform and logger have no `init` member of any kind, because both are **shared** providers that are never owned by a context.

## Allocator

```c
typedef struct pubnub_allocator_provider {
    void* (*alloc)(struct pubnub_allocator_provider* self, size_t size, size_t align);

    void* (*realloc)(struct pubnub_allocator_provider* self,
                     void*                             ptr,
                     size_t                            old_size,
                     size_t                            new_size,
                     size_t                            align);

    void (*free)(struct pubnub_allocator_provider* self, void* ptr);

    pubnub_buffer_t (*buf_acquire)(struct pubnub_allocator_provider* self,
                                   pubnub_buf_purpose_t              purpose);

    void (*buf_release)(struct pubnub_allocator_provider* self,
                        pubnub_buffer_t*                  buf);

    int (*buf_grow)(struct pubnub_allocator_provider* self,
                    pubnub_buffer_t*                  buf,
                    size_t                            new_cap);

    int (*init)(struct pubnub_allocator_provider* self,
                struct pubnub_platform_provider*  platform);

    void (*deinit)(struct pubnub_allocator_provider* self,
                   struct pubnub_platform_provider*  platform);
} pubnub_allocator_provider_t;
```

**Header:** `#include <pubnub/providers/allocator.h>` **Mandatory:** `alloc`, `free`, `buf_acquire`, `buf_release`. **Optional:** `realloc`, `buf_grow`, `init`, `deinit`.

The allocator has two tiers. General allocation (`alloc`, `realloc`, `free`) covers arbitrary sizes. Purpose-tagged buffers (`buf_acquire`, `buf_release`, `buf_grow`) let an arena-style backend hand out pre-partitioned regions tagged `PUBNUB_BUF_RX` (incoming HTTP response bodies), `PUBNUB_BUF_OBJ` (outgoing JSON request bodies), or `PUBNUB_BUF_SCRATCH` (temporary working memory):

```c
typedef enum pubnub_buf_purpose {
    PUBNUB_BUF_RX = 0,
    PUBNUB_BUF_OBJ,
    PUBNUB_BUF_SCRATCH
} pubnub_buf_purpose_t;

typedef struct pubnub_buffer {
    uint8_t*             data;
    size_t               len;
    size_t               cap;
    pubnub_buf_purpose_t purpose;
} pubnub_buffer_t;
```

`buf_acquire` sets `data = NULL` and `cap = 0` when the requested region cannot be acquired, rather than failing loudly. `alloc`/`realloc` return `NULL` on failure. `buf_grow` and `init` both return `0` on success, non-zero on failure.

:::warning The allocator's lifecycle hooks take different arguments from every other family, twice over
Every other per-context provider's `init`/`deinit` take the `pubnub_provider_deps_t` bundle described above. The allocator's do not:
```c
/* Transport and serialization — the common shape */
int  (*transport_init)(struct pubnub_transport_provider* self, const pubnub_provider_deps_t* deps);
void (*transport_deinit)(struct pubnub_transport_provider* self);         /* ONE argument */
/* Allocator — the outlier */
int  (*allocator_init)(struct pubnub_allocator_provider* self, struct pubnub_platform_provider* platform);
void (*allocator_deinit)(struct pubnub_allocator_provider* self, struct pubnub_platform_provider* platform);
```
The allocator takes a bare `pubnub_platform_provider*` instead of the `deps` bundle, and its `deinit` takes **two** parameters where every other family's takes one. This is because the allocator is resolved and initialized before the `pubnub_provider_deps_t` bundle exists: the bundle's own `allocator` field would otherwise be self-referential. If you write more than one custom provider, do not copy the shape of your first onto your allocator, or your allocator's second onto anything else.
:::

### Degradation when optional members are absent

* Missing `realloc`: the SDK falls back to `alloc` + copy + `free`.
* Missing `buf_grow`: a grow request fails with `PUBNUB_ERR_BUFFER_TOO_SMALL`, because a backend with fixed partitions has no way to grow one.
* Missing `init`/`deinit`: no per-provider setup or teardown runs; this is the common case for a heap-backed allocator with no state of its own to initialize.

### Sample code

The following is original code written for this page, implementing only the mandatory members plus the allocator's distinctive `init`/`deinit` signature. It is a minimal illustration, not a production allocator: a real implementation should size and reuse `buf_acquire` regions rather than allocating and freeing on every call.

```c
#include <pubnub/providers/allocator.h>
#include <pubnub/providers/platform.h>

#include <stddef.h>
#include <stdlib.h>

static void* demo_alloc(struct pubnub_allocator_provider* self, size_t size, size_t align)
{
    (void)self;
    (void)align;
    return malloc(size);
}

static void demo_free(struct pubnub_allocator_provider* self, void* ptr)
{
    (void)self;
    free(ptr);
}

static pubnub_buffer_t demo_buf_acquire(struct pubnub_allocator_provider* self,
                                         pubnub_buf_purpose_t              purpose)
{
    pubnub_buffer_t buf = { 0 };
    (void)self;

    buf.data    = (uint8_t*)malloc(4096);
    buf.cap     = (buf.data != NULL) ? 4096 : 0;
    buf.purpose = purpose;
    return buf;
}

static void demo_buf_release(struct pubnub_allocator_provider* self, pubnub_buffer_t* buf)
{
    (void)self;
    free(buf->data);
    buf->data = NULL;
    buf->cap  = 0;
}

/* Takes a platform provider, not a pubnub_provider_deps_t bundle — see
 * the allocator lifecycle warning above. */
static int demo_allocator_init(struct pubnub_allocator_provider* self,
                                struct pubnub_platform_provider*  platform)
{
    (void)self;
    (void)platform;
    return 0;
}

static void demo_allocator_deinit(struct pubnub_allocator_provider* self,
                                   struct pubnub_platform_provider*  platform)
{
    (void)self;
    (void)platform;
}

static pubnub_allocator_provider_t demo_allocator = {
    .alloc       = demo_alloc,
    .realloc     = NULL,      /* optional — the SDK falls back to alloc+copy+free */
    .free        = demo_free,
    .buf_acquire = demo_buf_acquire,
    .buf_release = demo_buf_release,
    .buf_grow    = NULL,      /* optional — a grow request fails with PUBNUB_ERR_BUFFER_TOO_SMALL */
    .init        = demo_allocator_init,
    .deinit      = demo_allocator_deinit,
};
```

The only worked, SDK-shipped example that injects a custom allocator is the arena backend used on embedded targets. [Environment Setup — Arena allocator](https://www.pubnub.com/docs/sdks/c/environment-setup.md#arena-allocator) walks through `pubnub_arena_allocator_init()` and the full two-zone design behind it, including its single-tenant constraint. That backend's source lives under `src/providers/allocator/`, which is not public API: read it for the pattern, not as a sanctioned public sample.

## Transport

```c
typedef void pubnub_transport_handle_t;

typedef struct pubnub_transport_provider {
    pubnub_transport_handle_t* (*send)(struct pubnub_transport_provider* self,
                                       pubnub_http_request_t*  request,
                                       pubnub_http_response_t* response);

    int (*poll)(struct pubnub_transport_provider* self, unsigned int timeout_ms);

    void (*cancel)(struct pubnub_transport_provider* self,
                   pubnub_transport_handle_t*        transport_handle);

    int (*init)(struct pubnub_transport_provider* self,
                const pubnub_provider_deps_t*     deps);

    void (*deinit)(struct pubnub_transport_provider* self);

    void (*wake)(struct pubnub_transport_provider* self);

    pubnub_res_t (*set_dns_servers)(struct pubnub_transport_provider* self,
                                    const char*                       primary,
                                    const char*                       secondary);

    void (*set_tls_ca_bundle)(struct pubnub_transport_provider* self,
                              const char*                       ca_pem);

    void (*set_tls_verify)(struct pubnub_transport_provider* self,
                           uint8_t                           skip_verify);
} pubnub_transport_provider_t;
```

**Header:** `#include <pubnub/providers/transport.h>` (wire types in `pubnub/providers/transport_types.h`) **Mandatory:** `send`, `poll`, `cancel`. **Optional:** `init`, `deinit`, `wake`, `set_dns_servers`, `set_tls_ca_bundle`, `set_tls_verify`.

* `wake(self)` — interrupts a blocking `poll()` from another thread, so a background thread picks up newly-enqueued work immediately instead of waiting for the next `PUBNUB_CFG_MAX_POLL_MS` timeout boundary. Implementations must be safe to call from any thread at any time, and a spurious wake must be harmless. `NULL` means the background thread simply discovers new work at the next poll-timeout boundary; nothing breaks, it just isn't instant.
* `set_dns_servers(self, primary, secondary)` — backs `pubnub_set_dns_servers()` (see [Utility Methods — DNS servers](https://www.pubnub.com/docs/sdks/c/api-reference/misc.md#dns-servers)). `NULL` `primary` means revert to system DNS discovery. Return `PUBNUB_ERR_NOT_SUPPORTED` if the transport can't honor custom DNS servers; `NULL` here means the same thing. The shipped `curl` transport implements this, but honoring it also depends on libcurl being built with c-ares — see [Troubleshooting](https://www.pubnub.com/docs/sdks/c/troubleshooting.md) for that failure mode.
* `set_tls_ca_bundle(self, ca_pem)` — replaces the CA bundle used for TLS verification with a PEM certificate chain (or `NULL` to revert to the platform's system store). Takes effect on connections opened after the call; in-flight connections are unaffected. `NULL` here means the transport doesn't support runtime CA replacement.
* `set_tls_verify(self, skip_verify)` — a non-zero `skip_verify` disables TLS peer certificate verification; zero re-enables it. `NULL` means the transport doesn't support toggling verification at runtime. Never ship with verification disabled: doing so exposes connections to man-in-the-middle attacks.

`send` takes ownership of driving one request to completion. The caller keeps `request` valid until completion is signalled through `poll()`, or until the request is cancelled. A middleware in the chain may mutate the request in place before delegating (see [Middleware chaining](#middleware-chaining) below), which is why the request pointer is non-`const`.

On immediate failure, `send` returns `NULL` and must set `response->completion = PUBNUB_HTTP_ERROR` before returning. `poll` must support `timeout_ms = 0` as a non-blocking check, for cooperative and no-thread targets. It returns the number of requests that completed, or a negative value on a fatal provider error. After `cancel` returns, the transport handle is invalid and the associated response will not be written.

The request and response descriptors your `send` receives:

```c
typedef struct pubnub_http_request {
    pubnub_http_method_t  method;
    const char*           host;
    uint8_t               secure;
    uint8_t               external;
    uint8_t               follow_redirects;
    uint8_t               compress_body;
    pubnub_string_view_t  path_segments[PUBNUB_CFG_HTTP_MAX_PATH_SEGMENTS];
    unsigned int          path_segment_count;
    pubnub_kv_t           query_params[PUBNUB_CFG_HTTP_MAX_QUERY_PARAMS];
    unsigned int          query_param_count;
    pubnub_kv_t           headers[PUBNUB_CFG_HTTP_MAX_HEADERS];
    unsigned int          header_count;
    char                  scratch[PUBNUB_CFG_HTTP_SCRATCH_SIZE];
    unsigned int          scratch_used;
    const uint8_t*        body;
    size_t                body_len;
    uint32_t              timeout_ms;
} pubnub_http_request_t;

typedef struct pubnub_http_response {
    const uint8_t*            body;
    size_t                    body_len;
    pubnub_kv_t               headers[PUBNUB_CFG_HTTP_MAX_RESP_HEADERS];
    unsigned int              header_count;
    pubnub_http_completion_t  completion;
    int                       status_code;
    pubnub_res_t              transport_error;
} pubnub_http_response_t;
```

Path segments and query parameter values arrive already percent-encoded. Your transport must not re-encode them, only concatenate them. `response->body` is owned by the transport and is valid until the next `send()` or `cancel()` on the same handle, or until the provider is destroyed. When `status_code == 0`, a transport may populate `body`/`body_len` with diagnostic text of its own, surfaced through `pubnub_response_error_message()`.

### Middleware chaining

A middleware **is** a `pubnub_transport_provider_t`. It embeds the vtable as the first member of its own struct, so a plain `pubnub_transport_provider_t*` cast reaches it. It holds a `next` pointer to the transport it wraps, and mutates the request in `send()` before delegating to `next->send()`. `poll()` and `cancel()` are always delegated to `next` unchanged. Only `send()` enriches the request.

The SDK chains up to six middlewares of its own around whatever transport you configure. Three are unconditional:

* A pnsdk-identification middleware, appending `pnsdk=PubNub-C/<version>`.
* A user-ID middleware, appending `uuid=<user_id>`.
* An auth middleware, appending `auth=<token>` when set.

Three more are added only when their condition holds:

* A retry middleware, only when `PUBNUB_ENABLE_RETRY` is on and the configured policy is not `PUBNUB_RETRY_NONE`. It's the only one of the six that also intercepts `poll`/`cancel`, since it needs to re-dispatch on backoff.
* A signature middleware, only when `PUBNUB_ENABLE_PAM` is on and `secret_key` is set, appending the PAMv3 `signature=` query parameter.
* A compression middleware, only when `PUBNUB_ENABLE_REQUEST_COMPRESSION` is on, compressing the request body per-request when `pubnub_http_request_t.compress_body` is set.

From outermost to innermost, the order is: auth, pnsdk, user-ID, retry (if present), signature (if present), compression (if present), then your transport. Signature wraps compression rather than the other way around, so its HMAC always covers the uncompressed body.

A transport you supply through `cfg.transport` becomes the **innermost** link this chain wraps. You get all of the applicable request-enrichment and retry behavior for free without reimplementing any of it. Whether a consumer can insert a middleware of their own *between* two of these SDK-owned layers is not something the public API exposes.

There is only one `pubnub_config_t.transport` field, and the SDK's chain wraps whatever it points to. If you want your own middleware, wrap your own inner transport with it yourself, using the same first-member-embedding pattern, and pass the outer wrapper as `cfg.transport`. It still becomes the innermost link from the SDK's perspective.

### Sample code

No transport provider ships as an example anywhere in the SDK tree. The following is original code written for this page. It is a toy transport that completes every request synchronously with a canned response and performs no real network I/O, so replace `send`/`poll`/`cancel` with a real HTTP client before using it. A middleware wraps it to append a custom header, demonstrating the embedding pattern above.

```c
#include <pubnub/client.h>

#include <stddef.h>
#include <string.h>

static pubnub_transport_handle_t* stub_send(struct pubnub_transport_provider* self,
                                             pubnub_http_request_t*           request,
                                             pubnub_http_response_t*          response)
{
    static const uint8_t canned_body[] = "{}";
    (void)self;
    (void)request;

    response->body         = canned_body;
    response->body_len     = sizeof(canned_body) - 1;
    response->header_count = 0;
    response->completion   = PUBNUB_HTTP_COMPLETE;
    response->status_code  = 200;
    return (pubnub_transport_handle_t*)&canned_body; /* non-NULL opaque handle */
}

static int stub_poll(struct pubnub_transport_provider* self, unsigned int timeout_ms)
{
    (void)self;
    (void)timeout_ms;
    return 0; /* stub_send() above already completed inline */
}

static void stub_cancel(struct pubnub_transport_provider* self,
                         pubnub_transport_handle_t*        transport_handle)
{
    (void)self;
    (void)transport_handle;
}

static pubnub_transport_provider_t stub_transport = {
    .send   = stub_send,
    .poll   = stub_poll,
    .cancel = stub_cancel,
    .init   = NULL,
    .deinit = NULL,
};

/* Embeds the vtable as its first member so the SDK core (and this
 * page's own code) can address it through a plain
 * pubnub_transport_provider_t* — the same pattern the SDK's own
 * enrichment middlewares use internally. */
typedef struct pn_header_middleware {
    pubnub_transport_provider_t  base;
    pubnub_transport_provider_t* next;
    const char*                  header_name;
    const char*                  header_value;
} pn_header_middleware_t;

static pubnub_transport_handle_t* header_mw_send(struct pubnub_transport_provider* self,
                                                  pubnub_http_request_t*           request,
                                                  pubnub_http_response_t*          response)
{
    pn_header_middleware_t* mw = (pn_header_middleware_t*)self;

    if (request->header_count < PUBNUB_CFG_HTTP_MAX_HEADERS) {
        pubnub_kv_t* header = &request->headers[request->header_count++];
        header->key.ptr     = mw->header_name;
        header->key.len     = strlen(mw->header_name);
        header->value.ptr   = mw->header_value;
        header->value.len   = strlen(mw->header_value);
    }

    return mw->next->send(mw->next, request, response);
}

static int header_mw_poll(struct pubnub_transport_provider* self, unsigned int timeout_ms)
{
    /* poll() and cancel() are always delegated to the inner transport
     * unchanged — only send() enriches the request. */
    pn_header_middleware_t* mw = (pn_header_middleware_t*)self;
    return mw->next->poll(mw->next, timeout_ms);
}

static void header_mw_cancel(struct pubnub_transport_provider* self,
                              pubnub_transport_handle_t*        transport_handle)
{
    pn_header_middleware_t* mw = (pn_header_middleware_t*)self;
    mw->next->cancel(mw->next, transport_handle);
}

static pn_header_middleware_t header_mw = {
    .base = {
        .send   = header_mw_send,
        .poll   = header_mw_poll,
        .cancel = header_mw_cancel,
        .init   = NULL,
        .deinit = NULL,
    },
    .next         = &stub_transport,
    .header_name  = "X-App-Build",
    .header_value = "1.4.2",
};

static void wire_custom_transport(void)
{
    pubnub_config_t cfg = pubnub_config_defaults();
    cfg.subscribe_key = "demo";
    cfg.user_id       = "my_unique_user_id";
    cfg.transport     = &header_mw.base; /* the SDK's own chain wraps this */
}
```

Reference implementations for a complete, real transport ship under `src/providers/transport/` (`curl/`, the single-file libcurl backend used by the `full` and `minimal` profiles, and `socket/`, the multi-file raw-socket backend the `embedded` profile selects). Neither directory is public API: study the pattern, do not present it as a sanctioned sample.

## Serialization

```c
typedef struct pubnub_json_value pubnub_json_value_t;

typedef struct pubnub_serialization_provider {
    pubnub_json_value_t* (*parse)(struct pubnub_serialization_provider* self,
                                  const uint8_t*                        data,
                                  size_t                                len);

    pubnub_res_t (*serialize)(struct pubnub_serialization_provider* self,
                              const pubnub_json_value_t*            value,
                              uint8_t*                              buf,
                              size_t                                buf_len,
                              size_t*                               out_len);

    void (*value_destroy)(struct pubnub_serialization_provider* self,
                          pubnub_json_value_t*                  value);

    int (*init)(struct pubnub_serialization_provider* self,
                const pubnub_provider_deps_t*         deps);

    void (*deinit)(struct pubnub_serialization_provider* self);

    pubnub_json_value_t* (*value_create_object)(struct pubnub_serialization_provider* self);
    pubnub_json_value_t* (*value_create_array)(struct pubnub_serialization_provider* self);
    pubnub_json_value_t* (*value_create_null)(struct pubnub_serialization_provider* self);

    pubnub_json_value_t* (*value_create_bool)(struct pubnub_serialization_provider* self,
                                              int truthy);

    pubnub_json_value_t* (*value_create_int)(struct pubnub_serialization_provider* self,
                                             int v);

    pubnub_json_value_t* (*value_create_double)(struct pubnub_serialization_provider* self,
                                                double v);

    pubnub_json_value_t* (*value_create_string)(struct pubnub_serialization_provider* self,
                                                const char* str,
                                                size_t      len);

    pubnub_json_value_t* (*value_create_string_view)(struct pubnub_serialization_provider* self,
                                                     const char*                           str,
                                                     size_t                                len);

    pubnub_json_value_t* (*value_create_raw)(struct pubnub_serialization_provider* self,
                                             const uint8_t* bytes,
                                             size_t         len);

    pubnub_res_t (*object_set)(struct pubnub_serialization_provider* self,
                               pubnub_json_value_t*                  obj,
                               const char*                           key,
                               size_t                                key_len,
                               pubnub_json_value_t*                  child);

    pubnub_res_t (*array_append)(struct pubnub_serialization_provider* self,
                                 pubnub_json_value_t*                  arr,
                                 pubnub_json_value_t*                  item);

    pubnub_res_t (*object_remove)(struct pubnub_serialization_provider* self,
                                  pubnub_json_value_t*                  obj,
                                  const char*                           key,
                                  size_t key_len);

    pubnub_res_t (*array_remove)(struct pubnub_serialization_provider* self,
                                 pubnub_json_value_t*                  arr,
                                 size_t                                index);

    pubnub_res_t (*object_reserve)(struct pubnub_serialization_provider* self,
                                   pubnub_json_value_t*                  obj,
                                   size_t                                n);

    pubnub_res_t (*array_reserve)(struct pubnub_serialization_provider* self,
                                  pubnub_json_value_t*                  arr,
                                  size_t                                n);

    pubnub_json_type_t (*value_type)(const pubnub_json_value_t* value);

    const char* (*value_as_string)(const pubnub_json_value_t* value,
                                   size_t*                    out_len);

    pubnub_res_t (*value_as_int)(const pubnub_json_value_t* value, int* out);
    pubnub_res_t (*value_as_double)(const pubnub_json_value_t* value, double* out);
    pubnub_res_t (*value_as_bool)(const pubnub_json_value_t* value, int* out_truthy);

    pubnub_json_value_t* (*object_get)(const pubnub_json_value_t* obj,
                                       const char*                key,
                                       size_t                     key_len);

    size_t (*object_size)(const pubnub_json_value_t* obj);
    pubnub_json_value_t* (*array_get)(const pubnub_json_value_t* arr, size_t index);
    size_t (*array_size)(const pubnub_json_value_t* arr);

    int (*object_iter_init)(const pubnub_json_value_t* obj,
                            pubnub_json_iter_t*        iter);

    int (*object_iter_next)(pubnub_json_iter_t*   iter,
                            const char**          out_key,
                            size_t*               out_key_len,
                            pubnub_json_value_t** out_value);
} pubnub_serialization_provider_t;
```

**Header:** `#include <pubnub/providers/serialization.h>` **Mandatory:** `parse`, `serialize`, `value_destroy`. **Optional:** every other member, individually, including `init`/`deinit`. Callers null-check each one before use.

`pubnub_json_value_t` is opaque. Your backend owns the concrete layout and only ever hands the SDK core a pointer it casts back to its own type. `parse` returns a caller-owned tree, or `NULL` on a parse or allocation failure. Release it with `value_destroy`. `value_create_string` copies its input into provider-owned storage, so the caller's buffer may be released immediately after the call returns.

`value_create_string_view` does not copy, so the caller must keep the string alive for the whole tree's lifetime. A backend with no non-copying representation may leave this member `NULL`. `object_set` and `array_append` transfer ownership of the child node to the parent on success, so releasing an already-transferred node is a use-after-free. Iterators (`pubnub_json_iter_t`) are invalidated by any mutation of the object being walked. The SDK does not enforce this, so caller discipline is the only guard.

`array_remove` shifts later elements down, invalidating any `array_get` pointer obtained at a higher index before the removal.

### Degradation when optional members are absent

* Missing `value_create_double`: expected when `PUBNUB_CFG_JSON_DOUBLE` is off, or the backend has no floating-point support (for example, an FPU-less target). When `PUBNUB_CFG_JSON_DOUBLE=0`, `value_create_double` and `value_as_double` are set to `NULL` together by the SDK's own shipped backends. Write a custom backend the same way: leave both `NULL` together, not just one of them.
* Missing `object_reserve`/`array_reserve`: the caller proceeds straight to `object_set`/`array_append`, which still fails with `PUBNUB_ERR_OUT_OF_MEMORY` if capacity is genuinely exhausted. There is simply no early-fail check available.

### Sample code

No serialization provider ships as an example anywhere in the SDK tree. The following is original code written for this page: a minimal backend implementing only the three mandatory members. It round-trips raw bytes rather than parsing real JSON structure, so it cannot back any feature that builds or walks a value tree (App Context, options structs with a JSON-tree field, and so on). A backend intended for general use needs the full set of constructors, mutators, and accessors, which the shipped cjson and jsmn backends under `src/providers/serialization/` implement in full.

```c
#include <pubnub/client.h>

#include <stdlib.h>
#include <string.h>

typedef struct pn_demo_json_node {
    uint8_t* bytes;
    size_t   len;
} pn_demo_json_node_t;

static pubnub_json_value_t* demo_parse(struct pubnub_serialization_provider* self,
                                        const uint8_t*                        data,
                                        size_t                                len)
{
    pn_demo_json_node_t* node;
    (void)self;

    node = (pn_demo_json_node_t*)malloc(sizeof(*node));
    if (node == NULL) {
        return NULL;
    }
    node->bytes = (len > 0) ? (uint8_t*)malloc(len) : NULL;
    if (node->bytes == NULL && len > 0) {
        free(node);
        return NULL;
    }
    memcpy(node->bytes, data, len);
    node->len = len;
    return (pubnub_json_value_t*)node;
}

static pubnub_res_t demo_serialize(struct pubnub_serialization_provider* self,
                                    const pubnub_json_value_t*            value,
                                    uint8_t*                              buf,
                                    size_t                                buf_len,
                                    size_t*                               out_len)
{
    const pn_demo_json_node_t* node = (const pn_demo_json_node_t*)value;
    (void)self;

    if (node->len > buf_len) {
        return PUBNUB_ERR_BUFFER_TOO_SMALL;
    }
    memcpy(buf, node->bytes, node->len);
    *out_len = node->len;
    return PUBNUB_OK;
}

static void demo_value_destroy(struct pubnub_serialization_provider* self,
                                pubnub_json_value_t*                  value)
{
    pn_demo_json_node_t* node = (pn_demo_json_node_t*)value;
    (void)self;

    if (node != NULL) {
        free(node->bytes);
        free(node);
    }
}

static pubnub_serialization_provider_t demo_serialization = {
    .parse         = demo_parse,
    .serialize     = demo_serialize,
    .value_destroy = demo_value_destroy
    /* Every other member — init/deinit and every constructor, mutator,
     * and accessor — is left at its zero-initialized default, which for
     * a function pointer is NULL. Designated-initializer syntax only
     * needs to name the mandatory three. */
};
```

## Platform

```c
typedef struct pubnub_platform_provider {
    pubnub_milliseconds_t (*monotonic_ms)(struct pubnub_platform_provider* self);

    pubnub_milliseconds_t (*wall_clock_ms)(struct pubnub_platform_provider* self);

    void (*sleep_ms)(struct pubnub_platform_provider* self, uint32_t ms);

    int (*random_bytes)(struct pubnub_platform_provider* self,
                        uint8_t*                         buf,
                        size_t                            len);

    void (*secure_zero)(struct pubnub_platform_provider* self, void* buf, size_t len);

    size_t (*lock_size)(struct pubnub_platform_provider* self);
    int (*lock_init)(struct pubnub_platform_provider* self, pubnub_lock_t* lock);
    void (*lock_destroy)(struct pubnub_platform_provider* self, pubnub_lock_t* lock);
    void (*lock_acquire)(struct pubnub_platform_provider* self, pubnub_lock_t* lock);
    void (*lock_release)(struct pubnub_platform_provider* self, pubnub_lock_t* lock);

    void* (*thread_create)(struct pubnub_platform_provider*  self,
                           struct pubnub_allocator_provider* allocator,
                           void (*fn)(void*),
                           void* arg);

    void (*thread_join)(struct pubnub_platform_provider*  self,
                        struct pubnub_allocator_provider* allocator,
                        void*                             thread_handle);

    pubnub_res_t (*file_load)(struct pubnub_platform_provider*  self,
                              const char*                       path,
                              struct pubnub_allocator_provider* allocator,
                              uint8_t**                         out_data,
                              size_t*                           out_len);
} pubnub_platform_provider_t;
```

**Header:** `#include <pubnub/providers/platform.h>` **Mandatory:** `monotonic_ms`, `wall_clock_ms`, `sleep_ms`, `random_bytes`. A `NULL` in any of these four fails `pubnub_create()`/`pubnub_init()` with `PUBNUB_ERR_PROVIDER_MISSING`, the same as the other three validated families. **Optional, all-or-nothing per group:** a lock group (`lock_size`, `lock_init`, `lock_destroy`, `lock_acquire`, `lock_release`, all-or-nothing across all five) and a thread group (`thread_create` and `thread_join`, where a non-`NULL` `thread_create` requires a non-`NULL` `thread_join` too). **Optional, independently:** `secure_zero`, `file_load`.

The platform provider is **shared**, not per-context: the SDK never calls a lifecycle callback on it, and multiple contexts may reference the same instance. There is no `init`/`deinit` member on this vtable at all. All locks are non-recursive: acquiring a lock already held by the same thread is undefined behavior.

`monotonic_ms` and `wall_clock_ms` are two different clocks with two different jobs, and mixing them up breaks Access Manager signing:

* `monotonic_ms` returns milliseconds since an arbitrary fixed point (typically boot). It has **no relationship to wall-clock time** and must never wrap or decrease within a session. It's used only for timeouts and deadlines.
* `wall_clock_ms` returns milliseconds since the Unix epoch (1970-01-01T00:00:00 UTC). It's used by the PAM signature middleware to generate the `timestamp=` HMAC signing parameter. Return `0` when wall-clock time isn't available (no RTC, not yet NTP-synchronized) — the PAM middleware treats `0` as an error and fails the signed request with `PUBNUB_ERR_NO_WALL_CLOCK`. A `NULL` `wall_clock_ms` member, by contrast, is caught earlier and differently: it fails context creation itself with `PUBNUB_ERR_PROVIDER_MISSING`, since the member is mandatory. See the note on this distinction under [How provider resolution and validation works](#how-provider-resolution-and-validation-works) above.

### Degradation when optional members are absent

* Missing lock group, or `PUBNUB_CFG_THREAD_SAFETY=0` at compile time: no lock is created, and same-context thread safety becomes the caller's responsibility. When `PUBNUB_CFG_THREAD_SAFETY=1` and the lock group is present, a per-context lock is created automatically; the compile-time flag and the vtable NULL-check both have to allow it.
* Missing thread group: the SDK silently skips starting its background thread (no error is returned), so subscribe callbacks fire only when you call `pubnub_process()` yourself. `pubnub_async()` still works without a background thread. It just doesn't fire until the next `pubnub_process()` call finds the future ready, instead of firing as soon as the transaction completes.
* Missing `secure_zero`: the SDK core falls back to a volatile `memset` loop.
* Missing `file_load`: `file_path` in `pubnub_send_file_opts_t` returns `PUBNUB_ERR_NOT_SUPPORTED`. See [File Sharing](https://www.pubnub.com/docs/sdks/c/api-reference/files.md) for how `PUBNUB_ENABLE_FILESYSTEM` gates whether the shipped POSIX, Windows, and FreeRTOS providers wire this member at all.

No example in the SDK tree injects a custom platform provider. Reference implementations ship under `src/providers/platform/`: `posix/` (implements every optional group, and is the only platform backend continuous integration exercises), `freertos/` (the embedded-target implementation), and `windows/`. Per [Environment Setup's CI-verified-platforms note](https://www.pubnub.com/docs/sdks/c/environment-setup.md#prerequisites), treat `freertos` and `windows` as source-verified rather than CI-verified.

## Crypto (provider)

```c
typedef struct pubnub_encrypted_data {
    uint8_t* data;
    size_t   data_len;
    uint8_t* metadata;
    size_t   metadata_len;
} pubnub_encrypted_data_t;

typedef struct pubnub_crypto_provider {
    uint8_t identifier[4];

    size_t (*encrypt_size)(struct pubnub_crypto_provider* self,
                           size_t                         plaintext_len);

    pubnub_res_t (*encrypt)(struct pubnub_crypto_provider* self,
                            const uint8_t*                 input,
                            size_t                         input_len,
                            pubnub_encrypted_data_t*       output);

    pubnub_res_t (*decrypt)(struct pubnub_crypto_provider* self,
                            const pubnub_encrypted_data_t* input,
                            uint8_t*                       output,
                            size_t*                        output_len);

    pubnub_res_t (*hmac_sha256)(struct pubnub_crypto_provider* self,
                                const uint8_t*                 key,
                                size_t                         key_len,
                                const uint8_t*                 data,
                                size_t                         data_len,
                                uint8_t*                       output,
                                size_t*                        output_len);

    int (*init)(struct pubnub_crypto_provider* self,
                const pubnub_provider_deps_t*  deps);

    void (*deinit)(struct pubnub_crypto_provider* self);
} pubnub_crypto_provider_t;
```

**Header:** `#include <pubnub/providers/crypto.h>`

This is one provider family among six, and it is the lowest of the two crypto layers. `pubnub_crypto_provider_t` above is the vtable a single algorithm implements. A `pubnub_crypto_module_t`, the type you actually attach to `pubnub_config_t.crypto_module`, wraps one such provider as its default cryptor plus a bounded list of fallback cryptors consulted only on decrypt. The module API, the two built-in algorithms, and the full migration story between them are in [Encryption](https://www.pubnub.com/docs/sdks/c/api-reference/encryption.md). This page covers only the raw vtable. If you are writing a custom algorithm, see [Encryption — Write a custom cryptor (the provider vtable)](https://www.pubnub.com/docs/sdks/c/api-reference/encryption.md#write-a-custom-cryptor-the-provider-vtable) for the full field-by-field contract, an original sample, and how to attach your provider to a module with `pubnub_crypto_module_create()`.

`encrypt`, `decrypt`, and `hmac_sha256` are each independently optional. `pubnub_init`/`pubnub_create` accept a crypto provider with any of the three left `NULL`, since crypto operations are opt-in per capability. A provider that only signs Access Manager requests needs just `hmac_sha256`. One that only encrypts payloads can omit `hmac_sha256` entirely.

`pubnub_config_t` has no raw crypto provider field. You always attach a crypto provider indirectly, wrapped in a `pubnub_crypto_module_t` and pointed to by `cfg.crypto_module`.

When `PUBNUB_ENABLE_CRYPTO` is on and `crypto_module` is non-`NULL`, the SDK calls `init` on the module's default cryptor and on every fallback cryptor during `pubnub_create()`/`pubnub_init()`. It calls `deinit` on all of them, in reverse order, during teardown. If one cryptor's `init` fails partway through attaching the module, every cryptor that already succeeded is rolled back with `deinit` before the error propagates. A crypto provider that is never wrapped in a module and never attached to a context has no `init`/`deinit` called at all.

## Logger

```c
typedef struct pubnub_logger_provider {
    void (*log)(struct pubnub_logger_provider* self,
                const pubnub_log_entry_t*      entry);

    void (*set_level)(struct pubnub_logger_provider* self,
                      pubnub_log_level_t             min_level);
} pubnub_logger_provider_t;
```

**Header:** `#include <pubnub/providers/logger.h>`

This is one provider family among six. It is a **shared** provider, like platform: the SDK never allocates, frees, or lifecycle-manages it, and it has no `init`/`deinit` member. `set_level` is optional. Every call site checks for `NULL` before invoking it, and there is no missing-member rejection test for either `log` or `set_level`, in either direction.

The vtable above is the provider *interface* a custom logger backend implements. The functions you call to register one against a context, `pubnub_logger_add`, `pubnub_logger_remove`, `pubnub_logger_remove_all`, and `pubnub_logger_log_level`, live in a different header, `#include <pubnub/log.h>`, not `pubnub/providers/logger.h`. Everything else about this vtable, including the log-level enum, the structured value tree, compile-time stripping, the registration API, and a full custom-logger sample, is in [Logging — Custom loggers](https://www.pubnub.com/docs/sdks/c/logging.md#custom-loggers).

## Writing and registering a custom provider

The shape is the same across all six families:

1. Embed the vtable as the **first member** of your own struct, so a pointer to your struct can be cast to and from the generic provider-pointer type.
2. Populate the mandatory members for that family.
3. Leave every optional member you do not need at `NULL` — omitting a field from a designated initializer does this for you automatically.
4. Assign the resulting pointer to the matching field on `pubnub_config_t` (`allocator`, `transport`, `serialization`, `platform`, `crypto_module`, or `logger`) before calling `pubnub_create()`/`pubnub_init()`. See [Configuration — Providers](https://www.pubnub.com/docs/sdks/c/api-reference/configuration.md#providers) for the field types and the `NULL`-selects-default rule.

Only the allocator family has a shipped, worked example that follows this pattern end to end: [examples/arena_echo](https://www.pubnub.com/docs/sdks/c/environment-setup.md#arena-allocator), which builds a `pubnub_arena_allocator_t` and passes `pubnub_arena_allocator_init(&arena, pool, sizeof(pool))` as `cfg.allocator`. Transport, serialization, platform, and crypto have no equivalent example anywhere in the SDK tree. For those families, build from the vtable contract on this page plus the shipped-backend source under `src/providers/`, which is not public API but is the best available reference for how a complete backend behaves.

## Next steps

* [Configuration](https://www.pubnub.com/docs/sdks/c/api-reference/configuration.md) — the `pubnub_config_t` provider fields, defaults, and the `PUBNUB_ERR_PROVIDER_MISSING` contract from the config-field angle.
* [Environment Setup](https://www.pubnub.com/docs/sdks/c/environment-setup.md) — `PUBNUB_PROVIDER_<FAMILY>` backend selection, build profiles, and cross-family configure-time conflicts.
* [Logging](https://www.pubnub.com/docs/sdks/c/logging.md) — the logger vtable from the consumer's side, with a full custom-logger sample.
* [Encryption](https://www.pubnub.com/docs/sdks/c/api-reference/encryption.md) — the crypto module API, built-in algorithms, and a full custom-cryptor sample.
* [Status Events](https://www.pubnub.com/docs/sdks/c/status-events.md) — the full `pubnub_res_t` result catalog, including `PUBNUB_ERR_PROVIDER_MISSING` and `PUBNUB_ERR_BUFFER_TOO_SMALL`.

## Terms in this document

* **PubNub** - PubNub is a real-time messaging platform that provides APIs and SDKs for building scalable applications. It handles the complex infrastructure of real-time communication, including: Message delivery and persistence, Presence detection, Access control, Push notifications, File sharing, Serverless processing with Functions and Events & Actions, Analytics and monitoring with BizOps Workspace, AI-powered insights with Illuminate.

Last updated at: 2026-09-24T12:25:41.000Z
