# Flutter Audience backend example

This small Node.js 22 example receives the minimised event produced by
`XaereBackendMeasurementReporter`, verifies a short-lived user JWT, restricts
the request to one configured Audience integration, then HMAC-signs the exact
body to Audience. It is intended to run with the publisher's existing backend,
not inside the Flutter application or on the Xaere VPS.

Download the reviewed reference implementation from
`https://docs.xaere.io/FLUTTER_BACKEND.js`.

Set these server-only variables in your own secret manager:

```text
XAERE_AUDIENCE_INGEST_URL=https://ingest.audience.xaere.io/v1/events
XAERE_AUDIENCE_INTEGRATION_ID=int_...
XAERE_AUDIENCE_HMAC_SECRET=xai_...        # returned once at integration creation
XAERE_RECEIVER_JWT_HS256_SECRET=<32+ random characters>
XAERE_RECEIVER_JWT_ISSUER=publisher-backend
XAERE_RECEIVER_JWT_AUDIENCE=xaere-receiver
XAERE_RECEIVER_JWT_MAX_LIFETIME_SECONDS=300
XAERE_AUDIENCE_UPSTREAM_TIMEOUT_MS=10000
```

Run `node server.js`. Expose the resulting endpoint over HTTPS behind the
publisher's normal gateway. The Flutter app sends an access token acquired
from that publisher's authentication system; it never receives the Audience
HMAC secret. The included HS256 verifier is a compact local example. Replace
it with the publisher's existing OIDC/JWKS verification in production when
app authentication already uses an identity provider.

The publisher must mint this reporting token only while Audience measurement
consent is active. It requires an `iat`, an `exp` no more than five minutes
later, `xaere_audience_consent: true`, and
`xaere_audience_integration_id: "int_..."` matching this backend's configured
integration. Consent withdrawal must stop minting tokens immediately. These
claims give the trusted backend an independent, short-lived consent and scope
gate; the Flutter-side toggle alone is not treated as server authorisation.

The proxy rejects unsolicited fields, a different integration ID, malformed
events, unauthenticated, long-lived or consentless calls, redirects and
responses with persistent device or account identifiers. The mobile request
requires `application/json`, reads at most 16 KiB of raw bytes, decodes strict
UTF-8 and signs and forwards the exact original Buffer. A multibyte character
split across network chunks therefore cannot change the HMAC input, while an
invalid UTF-8 sequence is rejected before any Audience connection.
The resulting Audience request
also requires the closed Core proof fields, binds that proof to the event XR
Code and integration, enforces its issuance/expiry window and rejects nested
tracking fields before computing the Audience HMAC. Audience independently
verifies the Core signature with its configured public key. Its request
is aborted after ten seconds by default (configurable from one to thirty
seconds), so a stalled ingestion service cannot hold mobile backend requests
open indefinitely. A timeout returns `504 audience_timeout`; the Flutter SDK
may retain the exact event only when the host explicitly configured its
encrypted consent-bound retry queue. Do not add identifiers, raw IP addresses
or precise location to this event path.

The proxy returns `202 {"accepted":true}` to Flutter only after the upstream
Audience service itself returned `202` with the closed
`accepted`/`duplicate`/`event_id` acknowledgement. A generic 2xx, malformed or
extended response, or an acknowledgement naming a different event, is treated
as `502 audience_unavailable`; an authenticated
Audience 4xx becomes the bounded `400 event_rejected`. Upstream bodies are
limited to 16 KiB and remain covered by the same abort deadline.

The Flutter receiver creates one random `event_id` per measurement and reuses
it across delivery retries. Preserve that value unchanged: do not replace it
in this proxy. Audience uses it as the exact idempotency key when an upstream
request succeeded but the mobile client did not receive the response.
