Client → API gateway (NestJS is the mandatory path)
Status: as of 2026-09-08. Binding architecture decision (PO/Alfredo, 2026-09-08; ref terminales-api #264). Scope: how the web and mobile clients reach backend data/services. Audience: all client (web/mobile) + API developers, and anyone reasoning about observability, security, or scaling.
The rule
Section titled “The rule”All client requests route through the NestJS API (terminales-api). Clients do NOT talk to Appwrite directly. Reads and writes from web and mobile go to the Nest gateway, which then talks to Appwrite (and the rest of the backend) server-side.
Why (the rationale that makes this binding)
Section titled “Why (the rationale that makes this binding)”- Observability — one choke point where every client request is traced, logged, and metered. Metrics/logs/traces are collected in NestJS. If clients hit Appwrite directly, that traffic is invisible to our monitoring; centralizing it makes debugging, capacity planning, and future decisions tractable.
- Security — a single guarded gateway (auth guards + rate-limiting + audit log) is the enforcement boundary, with Appwrite RLS as defense-in-depth rather than the only line. One surface to reason about and harden.
- Control — one place to shape, cache, version, and evolve traffic; scaling and behavioral changes happen server-side without touching every client.
The trade-offs (extra hop latency, NestJS carries read volume, large Storage blobs proxy through the API) were weighed and accepted — NestJS scales horizontally, and centralized observability/security is the priority.
The single sanctioned exception: Appwrite Realtime
Section titled “The single sanctioned exception: Appwrite Realtime”Appwrite Realtime is a client-direct WebSocket (client → Appwrite) and cannot be proxied through NestJS. It is the one allowed direct-to-Appwrite path, because live POS sync (a sale on one terminal reflecting on another) needs a push channel and polling is a poor substitute. Even here:
- Data writes still flow through NestJS (and are therefore observable); only the live push notification is direct.
- The Realtime leg is paired with an always-on poll backstop —
useRealtimePolledJob(terminales-web/src/core/appwrite/useRealtimePolledJob.ts) runs the Appwrite Realtime subscription and arefetchIntervalpoll concurrently, both writing the same cache idempotently, so a dropped/reconnecting socket never strands a job. - It is gated by the
appwrite_realtimeruntime flag (seeded ON/global in prod; OFF = emergency kill-switch that removes the WS leg, leaving the poll backstop to carry updates at interval latency — no redeploy). This is the only remaining runtime feature flag; see the flag register:terminales-api/docs/flags/appwrite-rollout.md.
Note: mobile does not yet have a shared equivalent of
useRealtimePolledJob— the offline-first Realtime+poll reconcile is specified but not yet built as a shared primitive (possible follow-up).
Consequences
Section titled “Consequences”- The former
appwrite_client_directfeature flag is retired — “clients direct to Appwrite” is permanently off by architecture, not a toggle. (One of the 7 flags retired in the #264 reconcile.) - New client features consume backend data via the NestJS API, not the Appwrite SDK directly — the sole exception being subscribing to Appwrite Realtime for live updates (behind
appwrite_realtime).
References
Section titled “References”terminales-api/docs/flags/appwrite-rollout.md— the reconciled 1-flag register + kill-switch/observability contract.- terminales-api #264 (rollout-ramp reconcile, closed 2026-09-08);
appwrite_client_directretirement.