Social LiveDocumentationPLATFORM DOCS

Networking Layer

The single client every request flows through -auto token refresh, UTF-8 safety, uploads, and the realtime socket.

Last updated Thu Jul 16 2026 00:00:00 GMT+0000 (Coordinated Universal Time)

TL;DR -Every request the app makes goes through one client (ApiClient). It quietly refreshes expired logins, sends text as proper UTF-8 so emoji and smart quotes never break, and handles uploads. A separate always-on socket carries realtime events (live rooms, chat).

Who this is for -Product & QA: the "In plain terms" section explains why sessions renew silently and why emoji-in-DMs once broke. Mobile & engineering: the refresh protocol, UTF-8 rules, error contract, upload path, socket layer, and build config.

Every time the app needs something from the server -your wallet, a chat message, a profile edit -it goes through a single shared piece of code called ApiClient. Funnelling everything through one door means the tricky, easy-to-get-wrong parts (renewing your login, encoding text correctly, handling errors) are solved once and reused everywhere.

Alongside the request/response client there's a second, always-open connection called a socket -used for things that need to happen live, like a gift appearing in a room the instant it's sent, without the app having to keep asking "anything new yet?".

In plain terms

Three things ApiClient handles that a reader should understand up front:

  • Silent re-login. Your access pass expires every hour, but you never get kicked out mid-tap. When a request comes back saying "expired," the client fetches a fresh pass behind the scenes and retries -so long as your refresh window (30 days) is still open. A flaky Wi-Fi moment never logs you out; only a genuinely rejected login does.
  • Emoji and smart-quotes work. Text is sent as UTF-8 (the modern universal text encoding). This sounds obvious, but a subtle default once caused emoji and iPhone curly quotes to crash requests before they even left the phone -so it's now an enforced rule.
  • Consistent errors. When the server says no, the app gets a clean error with the reason. Screens that involve money always surface the failure; browse/list screens may quietly fall back to sample data so the app still looks alive offline.

The socket is the app's "live wire": one connection per session, shared by every feature that needs realtime updates, and it automatically reconnects and re-joins your rooms if the connection drops.

ApiClient (lib/core/network/api_client.dart)

A thin dart:io HttpClient wrapper providing getJson, postJson, patchJson, deleteJson, and postMultipart. Everything the app sends goes through it.

Automatic 401 retry with single-flight refresh

This is the "silent re-login" from above, in detail. Access tokens live 1 hour. When any request returns 401:

  1. The client calls POST /auth/refresh with the stored refresh token.
  2. Single-flight: concurrent 401s share one refresh future -critical because refresh tokens are single-use and rotating; racing refreshes would revoke each other.
  3. The new session is persisted and the original request retries once.

UTF-8 discipline (learned the hard way)

This is the emoji bug, and the rule that fixed it. HttpClientRequest.write() encodes with Latin-1 when the Content-Type header carries no charset. Emoji and iOS smart punctuation ( U+2019) are outside Latin-1, so requests used to throw "Contains invalid characters" on-device before sending -breaking DMs and profile edits.

The fix, now the standing rule for any new request path:

  • Send bodies as explicit bytes: request.add(utf8.encode(jsonEncode(body))).
  • Declare Content-Type: application/json; charset=utf-8.
  • Multipart framing uses utf8.encode(...), never String.codeUnits.

Error contract

Non-2xx responses raise AppError carrying the HTTP status and the backend's message field when present. Repositories decide per-call whether to rethrow (money paths must) or fall back to fixture data (list/browse paths may, to keep the app demoable offline).

Uploads

postMultipart hand-builds the multipart body (avatar uploads). Files land in the backend's uploads/ volume, served at /uploads/*.

Socket layer

This is the "live wire." lib/modules/signaling/ owns the Socket.IO connection: connect with the JWT, expose streams of gateway events, auto-reconnect, and re-join rooms on reconnect. Feature modules (live room, chat) subscribe through it rather than opening their own sockets -one connection per app session.

Configuration

The app is pointed at a server at build time, not runtime. AppConfig reads compile-time defines:

flutter run \
  --dart-define=API_BASE_URL=http://127.0.0.1:3000/api \
  --dart-define=SIGNALING_URL=http://127.0.0.1:3000

Production builds default to the deployed API host.