Module 6 of 9 Middle layer
API Architectures
Why this matters for Univa
Every Univa app has a client (a browser, a mobile app, another service) that needs data from somewhere else. The "middle layer" that connects them, REST, GraphQL, or gRPC, decides how fast the app feels, how much bandwidth it burns, how easy it is to debug, and how much it costs to run. Pick the wrong one and you either over-engineer a simple SME site into something that needs a dedicated backend team, or under-engineer a data-heavy mobile app until it feels sluggish on cheap Malaysian mobile data plans.
Univa's default stack (Next.js route handlers talking to Supabase, per docs/app-building-philosophy.md) is quietly a REST-shaped choice, even though it rarely gets named that way.
This module explains why that default is correct for almost every Univa client, and gives you the vocabulary to explain, in a client meeting or a job interview, exactly when GraphQL or gRPC would actually earn their added complexity instead of just sounding impressive.
This also matters because "API architecture" questions show up constantly in technical due diligence. A client's existing engineering team, an investor's technical advisor, or a job description may ask "do you support GraphQL?" or "is this gRPC-based?" Knowing the honest, confident answer (usually: "REST is the right choice here, and here is why") protects Univa's credibility far better than either bluffing or reflexively adopting whatever sounds most modern.
Finally, this module matters because the "middle layer" question and the "realtime" question always travel together in a real build. The moment a client asks for a live dashboard, a chat feature, or a streaming AI response, the conversation about REST/GraphQL/gRPC becomes incomplete without also deciding between webhooks, SSE, WebSockets, or Supabase Realtime, which is why both are covered together here.
There is also a practical delivery angle worth naming upfront. Univa's Next.js route handlers already function as a lightweight Backend-for-Frontend layer, combining Supabase calls, authentication checks, and business logic into a response tailored to each screen. Recognizing this explicitly, rather than treating it as "just some API routes," is what lets you tell a client with confidence that Univa's architecture is a deliberate choice, not an accident of convenience.
Core concepts
What a middle layer actually is: the contract and wire format that lets a client (a browser, a mobile app, another server) ask a backend for data or tell it to do something, without either side needing to know the other's internal implementation. REST, GraphQL, and gRPC are three different answers to the same question: how should that contract be shaped, and how should the bytes travel over the network.
REST (Representational State Transfer) is not a protocol or a library, it is an architectural style built on top of plain HTTP.
A REST API exposes resources (a customer, an order, a product) at URLs, and uses standard HTTP verbs to act on them: GET to read, POST to create, PUT or PATCH to update, DELETE to remove.
The response is almost always JSON (JavaScript Object Notation), a human-readable text format that any language can parse.
REST's biggest strength is universality: every browser, every mobile OS, every programming language, and every monitoring or debugging tool already understands plain HTTP and JSON natively, with zero extra tooling required.
Its biggest weakness is over-fetching and under-fetching: a GET /orders/123 endpoint returns whatever fields the backend developer decided to include, whether the client needs all of them or not, and if the client needs data from two different resources (an order and its customer), it often has to make two separate requests.
GraphQL (created at Facebook/Meta, open-sourced 2015) is a query language for APIs, not a full architectural style by itself. Instead of many fixed endpoints, a GraphQL API typically exposes a single endpoint, and the client sends a query describing exactly which fields it wants, potentially across multiple related resources in one request. The server responds with exactly that shape, no more, no less. This solves REST's over-fetching and under-fetching problem directly: a mobile app on a slow connection can ask for just the three fields it actually renders on screen, instead of downloading an entire resource object with fields it will never use. The cost is added complexity: GraphQL needs a schema definition, a resolver layer that maps each field to actual data-fetching logic, and its own tooling for caching (since a single endpoint that varies its response per-request cannot be cached with the same simple HTTP caching rules REST enjoys).
gRPC (Google, open-sourced 2015) is a high-performance remote procedure call (RPC) framework, most commonly used for service-to-service communication rather than direct browser-to-server communication.
Instead of JSON over HTTP/1.1, gRPC uses Protocol Buffers (protobuf), a compact binary wire format, over HTTP/2, which supports multiplexed streaming connections.
A gRPC contract is defined in a .proto file that specifies exact function signatures (like calling a local function, but the actual code runs on a remote server), and code generation tools produce strongly-typed client and server code in whatever language each side uses.
The binary format and HTTP/2 transport make gRPC significantly faster and lighter on the wire than JSON-over-HTTP for high-volume internal traffic, and it supports true bidirectional streaming (both sides can keep sending messages over one open connection).
The cost: protobuf payloads are not human-readable in transit (you need the .proto schema and tooling to inspect them), browser support is historically weak (gRPC-Web exists but adds an extra translation layer), and the tooling overhead is not worth it unless the traffic volume or latency sensitivity actually demands it.
Wire format, side by side: REST typically ships JSON, a text format that is easy to read in a browser dev tools Network tab, easy to log, and easy to debug by eye, at the cost of being larger on the wire (every field name is repeated as plain text in every single response). gRPC ships protobuf, a binary format that is smaller and faster to parse, at the cost of needing generated code and tooling just to read what a message actually contains. GraphQL typically still rides on top of JSON for its actual data payloads, so it inherits JSON's readability, while solving the over-fetching problem at the query level rather than at the wire-format level.
Versioning: REST APIs commonly version through the URL path (/v1/orders, /v2/orders) or a request header, and a breaking change usually means shipping and maintaining a whole new version path.
GraphQL takes a different approach: fields are added over time and old fields are marked deprecated rather than removed, so a single schema can serve old and new client versions simultaneously without an explicit /v2 split, as long as deprecated fields are not deleted too early.
gRPC versions at the .proto schema level using field numbers; fields can be added freely, but removing or renumbering a field breaks every client still compiled against the old schema, so field numbers are treated as permanent once shipped.
Error handling: REST typically communicates errors through HTTP status codes (404 for not found, 400 for a bad request, 500 for a server error) plus a JSON error body with details.
GraphQL is unusual here: even a failed field lookup often still returns an HTTP 200 OK, with the actual error detail nested inside the JSON response body's errors array, which surprises developers used to REST's status-code-first convention.
gRPC uses its own status code system (a fixed enum of codes like NOT_FOUND, INVALID_ARGUMENT, UNAVAILABLE) that is richer and more structured than plain HTTP codes but is specific to the gRPC ecosystem and its tooling.
Caching: plain REST over HTTP gets browser and CDN caching almost for free, because a GET request to a stable URL can be cached using standard HTTP cache headers (Cache-Control, ETag), and edge networks like Cloudflare understand this natively.
GraphQL's single-endpoint, query-varies-per-request model breaks that simple model; GraphQL caching usually needs a dedicated client-side cache library (like Apollo Client or Relay) that caches at the level of individual fields and objects, which is powerful but is genuinely more setup work.
gRPC generally is not cached at the HTTP layer at all, since it is built for direct service-to-service calls rather than for content that benefits from a shared cache.
Authentication across the three: REST APIs commonly authenticate with API keys, JWTs, or session cookies, checked once at the endpoint or middleware level, since each endpoint has a fixed, known shape. GraphQL's single flexible endpoint means authentication alone is not enough; authorization often has to happen per-field inside resolvers, since a single query can legally touch many different resources with different access rules in one round trip. gRPC typically authenticates via mTLS (mutual TLS certificates) between trusted internal services, or bearer tokens carried in call metadata, reflecting its usual deployment inside a trusted internal network rather than the open internet. Supabase's own row-level security (RLS) policies, which Univa already leans on, are a database-level answer to this same problem: the authorization check lives with the data itself, regardless of which middle layer sits in front of it.
The Backend-for-Frontend (BFF) pattern is worth knowing as a named pattern, since it describes what a Next.js route handler layer already does for Univa without anyone calling it by that name. A BFF is a thin server layer, dedicated to one specific frontend (a web app, a mobile app), that sits between the client and one or more backend data sources, reshaping and combining data into exactly what that frontend needs. Next.js route handlers calling Supabase are, in effect, a lightweight BFF: they can combine multiple Supabase queries, apply business logic, and return a shape tailored to the page that calls them, without the client ever seeing Supabase directly. Recognizing this pattern is useful when a client asks about "a proper backend": the honest answer is that a well-organized BFF layer already covers most of what a separate backend service would provide at this project scale.
Common pitfalls when picking a middle layer, worth naming so they do not sneak into a Univa decision unnoticed:
- Adopting GraphQL for a project with one or two simple screens, where a handful of REST endpoints would have taken a fraction of the setup time and produced the exact same user-facing result.
- Adopting gRPC for anything a browser talks to directly, without first checking whether gRPC-Web's translation layer and weak tooling support are actually worth the wire-format speedup for that specific traffic pattern.
- Treating "REST" as a rigid rulebook rather than a style; plenty of production REST APIs bend the strict verb/resource conventions in pragmatic ways (a
POST /orders/123/cancelaction-style endpoint, for instance) and that is fine. - Forgetting that GraphQL still needs the same authentication and authorization discipline as REST; a single flexible endpoint does not remove the need to check who is allowed to see which fields.
- Building a custom WebSocket server for a realtime feature that Supabase Realtime already covers out of the box, adding an unnecessary extra moving part to maintain.
- Exposing the raw Supabase client directly to the browser for complex multi-step operations instead of wrapping them in a route handler, which loses the chance to validate, combine, and authorize the operation server-side.
A note on hybrid approaches: in practice, many production systems do not pick just one of these three exclusively. A large e-commerce platform might use REST for its public-facing catalog API (because it needs CDN caching), GraphQL for its own mobile app's data layer (because screens need precise field shaping), and gRPC purely between its internal inventory and pricing microservices (because that traffic is high-volume and entirely internal). For Univa's SME-scale projects this hybrid pattern rarely applies, since a single Next.js app with a Supabase backend has no internal microservice boundary to speak of, but it is worth recognizing the pattern when reading about how larger companies structure their systems.
The landscape (comparison tables)
Core comparison
| Dimension | REST | GraphQL | gRPC |
|---|---|---|---|
| Category | Architectural style over HTTP | Query language for APIs | RPC framework |
| Wire format | JSON (text) | JSON (text), over a single endpoint | Protocol Buffers (binary) |
| Transport | HTTP/1.1 or HTTP/2 | HTTP/1.1 or HTTP/2, single endpoint | HTTP/2 (required) |
| Human-readable in transit | Yes | Yes | No (needs .proto + tooling) |
| Over/under-fetching | Common problem | Solved by design (client picks fields) | Not applicable (fixed method signatures) |
| Streaming support | Limited (polling, or SSE/WebSockets bolted on) | Subscriptions (via WebSockets, separate mechanism) | Native bidirectional streaming |
| Browser support | Native, universal | Native, universal (just HTTP + JSON) | Weak (needs gRPC-Web translation layer) |
| Caching | Native via HTTP semantics | Needs a dedicated client cache library | Not typically cached |
| Learning curve | Low | Moderate | Moderate to high |
| Typical use case | Public APIs, client-server web/mobile apps | Mobile apps and complex frontends needing precise data shaping | Microservice-to-microservice, internal infra |
Tooling and ecosystem
| Concern | REST | GraphQL | gRPC |
|---|---|---|---|
| Schema/contract definition | Informal (OpenAPI/Swagger optional) | Required (GraphQL SDL schema) | Required (.proto files) |
| Code generation | Optional (from OpenAPI spec) | Common (typed client hooks from schema) | Standard practice (generates client/server stubs) |
| Popular server libraries | Express, Fastify, Next.js route handlers | Apollo Server, GraphQL Yoga, Pothos | grpc-node, grpc-go, grpc-java |
| Popular client libraries | fetch, Axios, TanStack Query | Apollo Client, Relay, urql | Generated stubs per language |
| Debugging tools | Browser dev tools, Postman, curl | GraphiQL / Apollo Studio playground | grpcurl, BloomRPC, generated clients |
| Supabase relevance | Supabase's REST (PostgREST) and client SDK are REST-shaped | Supabase offers a GraphQL extension, rarely needed for SME scale | Not part of Supabase's typical usage pattern |
Authentication and authorization comparison
| Approach | REST | GraphQL | gRPC |
|---|---|---|---|
| Typical auth mechanism | API keys, JWTs, session cookies | Same transport-level options as REST, checked before resolving | mTLS certificates, bearer tokens in call metadata |
| Where authorization is enforced | Per-endpoint or middleware | Per-endpoint plus per-field inside resolvers | Per-service, often at the network/mesh level |
| Typical deployment context | Public internet-facing | Public internet-facing | Trusted internal network |
| Fits well with Supabase row-level security | Yes, directly | Yes, but needs extra resolver-level checks too | Not typically used alongside Supabase |
Realtime option comparison
| Option | Direction | Transport | Best for | Complexity |
|---|---|---|---|---|
| Webhooks | Server pushes to server | HTTP POST callback | Notifying another system when an event happens (payment received, form submitted) | Low |
| SSE (Server-Sent Events) | Server pushes to client, one-way | Long-lived HTTP connection | Live status updates, streaming AI responses, notification feeds | Low to moderate |
| WebSockets | Full duplex, both directions | Persistent TCP-based connection | Chat, live cursors, collaborative editing, anything needing client-to-server pushes too | Moderate to high |
| Supabase Realtime | Full duplex (built on WebSockets + Postgres logical replication) | WebSocket, managed by Supabase | Live database row updates (new order appears instantly), presence, broadcast | Low (already included in the stack) |
Cost and infrastructure footprint
| Layer | Extra infrastructure needed beyond Next.js + Supabase | Typical hosting cost impact for SME scale |
|---|---|---|
| REST (route handlers + Supabase) | None | Fits free tiers comfortably |
| GraphQL | A GraphQL server process (Apollo Server or similar), or a managed GraphQL layer | Adds a service to run and monitor, usually a small but real recurring cost |
| gRPC | A gRPC-capable backend runtime and, for browser access, a gRPC-Web proxy | Adds meaningful infrastructure most SME apps do not otherwise need |
| SSE | None (built on standard HTTP) | Fits free tiers comfortably |
| WebSockets | A persistent-connection-capable host (not all serverless platforms support this natively) | Can require a dedicated server or a managed WebSocket provider |
| Supabase Realtime | None (included in Supabase) | Fits Supabase's free tier for typical SME traffic volumes |
How to choose (decision rules)
Picking REST vs GraphQL vs gRPC:
- If the project is a typical Univa SME web or mobile app with a Next.js frontend and a Supabase backend: use REST (in practice, Next.js route handlers plus the Supabase client, which is itself REST-shaped under the hood via PostgREST). This is the lowest-complexity option, it is cacheable, debuggable with plain browser tools, and every future hire will already know how to work with it.
- If the client is building a mobile app with genuinely tight bandwidth or battery constraints, and the app's screens each need very different, precisely-shaped slices of overlapping data (a common pattern in large social or e-commerce apps): GraphQL's exact-field queries pay off. The win is real when a single screen would otherwise require 3 to 5 separate REST calls, or when the same backend serves many different client screens each needing a different field subset from the same underlying resources.
- If two or more of Univa's own backend services need to talk to each other at high volume, with strict latency requirements, and both sides are services Univa controls (not a public-facing API): gRPC's performance pays off. This is the classic microservice-to-microservice case; it is rarely the right choice for anything a browser talks to directly, because of gRPC's weak native browser support.
- If a client asks for GraphQL "because it's more modern": push back gently and ask what specific problem they are trying to solve. If the honest answer is "our mobile screens keep needing custom REST endpoints for each view," that is a real GraphQL signal. If the honest answer is "we read a blog post," REST is still the right call, and adopting GraphQL there would only add setup cost, a new caching strategy, and a steeper onboarding curve for future hires, with no matching benefit.
- If a client asks for gRPC because "Google uses it": the same skepticism applies. gRPC's value is internal service-to-service speed at real scale; an SME app with one Next.js app and one Supabase database has no service-to-service problem for gRPC to solve.
- If a client's team already has a working GraphQL or gRPC backend and Univa is integrating with it rather than building it: use whatever the existing system speaks. Consistency with an already-live system beats a theoretically cleaner architecture choice.
- If Univa needs to combine and reshape data from multiple sources (Supabase plus a third-party API) before handing it to the client: that is exactly the Backend-for-Frontend pattern already in use via Next.js route handlers, not a signal to introduce GraphQL.
- If authorization rules are complex enough that field-level access control (some users can see field X, others cannot) is a recurring theme across the whole app: that is one of the few legitimate technical arguments for GraphQL over REST, since GraphQL resolvers naturally centralize field-level authorization. Even then, prefer solving it at the Supabase row-level security level first before adding an entirely new API layer.
- Default posture: start with REST, add GraphQL only when a concrete over-fetching problem is measured, add gRPC only when a concrete internal service-to-service latency problem is measured. Never adopt either as a starting default; both are justified by a specific, demonstrated pain point, not by general modernity.
Picking a realtime option (a common adjacent question once the middle layer is settled):
- If one backend system just needs to notify another system that something happened (a payment succeeded, a form was submitted) and no ongoing connection is needed: use a webhook.
- If the server needs to push a one-way stream of updates to the client (a progress indicator, a streaming AI chat response, a live notification feed) and the client never needs to push data back over that same channel: use SSE. SSE is simpler to implement and debug than WebSockets and works over plain HTTP.
- If the client and server both need to send messages back and forth continuously over the same connection (a chat app, live cursors, a collaborative editor): use WebSockets.
- If the app already runs on Supabase and the realtime need is "notify the UI the instant a database row changes" (a new order appears on a dashboard, a chat message arrives): use Supabase Realtime directly. It is already part of the stack, requires no separate infrastructure, and covers the vast majority of Univa's realtime needs without introducing a new moving part.
Univa playbook
- Default API layer: Next.js route handlers calling the Supabase client (or calling Supabase's REST/PostgREST layer directly), which is REST in practice even when it does not carry the "REST" label explicitly. This is the answer for essentially every Univa SME build: websites, internal tools, AI-powered apps.
- When to reach for GraphQL: almost never for a new Univa build. Reserve it for the rare case of a client with an existing complex mobile app suffering a measured over-fetching problem across many screens, or a client whose in-house team already runs a GraphQL backend that Univa is integrating with.
- When to reach for gRPC: essentially never for client-facing Univa work. It only becomes relevant if Univa ever builds a multi-service internal backend (for example, splitting an AI processing pipeline into separate internal services that must talk to each other at high volume), which is well beyond current SME project scale.
- Realtime default: use Supabase Realtime for anything living in a Supabase table (orders, chat messages, live dashboards). Use SSE for streaming AI-generated responses back to the browser (matches the "streaming AI = always" rule in
docs/app-building-philosophy.md). Reserve raw WebSockets for the rare case Supabase Realtime cannot cover, and reserve webhooks for server-to-server integrations (payment gateway callbacks, third-party form submissions). - Cost angle: REST plus Supabase Realtime keeps everything inside Vercel's/Cloudflare's free tier and Supabase's free tier for typical SME traffic, consistent with the freemium-tier-first, cost-conscious posture. GraphQL and gRPC both usually require additional dedicated infrastructure (a GraphQL server process, or a gRPC-capable backend service) that plain Next.js route handlers do not need, so both carry a real infra-cost tax beyond their added complexity.
- API design habit: even while staying REST-shaped, keep endpoints resource-oriented and predictable (
/api/orders,/api/orders/[id]) rather than ad hoc, and return only the fields a screen actually needs from a given endpoint where practical. This captures a meaningful share of GraphQL's efficiency benefit without any of its setup or caching complexity, simply through disciplined REST endpoint design. - BFF awareness: treat the Next.js route handler layer as Univa's de facto Backend-for-Frontend. Combine and reshape Supabase (and any third-party API) calls there rather than exposing raw database queries to the client, which captures most of the architectural benefit of a "proper backend" without adding a new service to run.
- Talking to clients: if a client's technical advisor asks "why not GraphQL," the honest, confident answer is: REST plus Supabase covers the actual data-fetching needs of an SME-scale app at a fraction of the setup and hosting cost, and GraphQL's benefits only show up at a scale and complexity this project has not reached, and may never reach.
Hands-on exercise
Pick any small existing Univa project (or a throwaway Next.js app) and do the following in roughly one evening:
- Build one REST-style route handler (
/api/products) that returns a hardcoded array of 5 product objects, each with 6 fields (id, name, price, description, category, image URL). - Write a small client component that fetches from it and renders only 2 of those 6 fields (name and price) on a product card, and note in one sentence how many "wasted" fields traveled over the network per product.
- Add a second route handler,
/api/products/[id]/full, that returns all 6 fields for one product, and call it from a "product detail" page, to feel the two-fetch pattern REST often needs when a list view and a detail view need different field sets. - As a thought exercise (no need to actually implement a GraphQL server), sketch on paper what a single GraphQL query would look like that fetches only
nameandpricefor the list view, and separately fetches all 6 fields for the detail view, from the same schema, in each case requesting only the fields actually rendered. - Add one working SSE endpoint (
/api/stream) that sends 3 messages a second apart (data: message 1,data: message 2,data: message 3), and a client component that displays each message as it arrives, to get hands-on with the realtime option Univa uses most for AI streaming. - Rename the
/api/productshandler's response shape mentally into a "BFF" framing: imagine it also had to merge in areviewsCountfield from a separate third-party reviews API, and write one sentence on where that merging logic should live (inside the route handler, not in the client component).
Stretch goal (if time allows): rewrite the /api/products route handler so it accepts an optional ?fields=name,price query parameter and only returns the requested fields, then compare that lightweight REST trick against the GraphQL query you sketched in step 4.
Note in one sentence how far simple query-parameter field selection gets you toward GraphQL's benefit, and where it would still fall short (nested relations across multiple resources, for instance).
Write two or three sentences afterward: where REST's over-fetching cost genuinely showed up in step 2 and 3, and whether it was severe enough, at this toy scale, to justify the extra GraphQL setup you sketched in step 4.
Self-check
- What specific problem does GraphQL solve that plain REST does not solve by design?
- Why is gRPC rarely used for direct browser-to-server communication, even though it is faster on the wire than REST or GraphQL?
- A client's mobile app team says: "every screen makes 4 to 5 REST calls and half the returned fields are unused." What API architecture change would you propose, and why?
- Why does REST cache more easily at the HTTP/CDN level than GraphQL does?
- A Univa client wants a live dashboard that updates the moment a new order is inserted into the Supabase
orderstable. Which realtime option fits with the least added infrastructure, and why?
Answers:
- GraphQL lets the client specify exactly which fields it needs in a single request, solving REST's over-fetching (extra unused fields returned) and under-fetching (needing multiple round trips to assemble related data) problems by design, rather than by convention or extra endpoints.
- gRPC relies on Protocol Buffers (a binary format) and HTTP/2 in a way browsers do not natively support well; using it from a browser requires an extra gRPC-Web translation layer, which erases much of its performance advantage and adds complexity that plain REST or GraphQL over standard HTTP does not require.
- Introduce GraphQL for that mobile app, since the stated symptom (many calls, mostly unused fields) is the exact over-fetching and under-fetching pattern GraphQL was built to solve, and the complexity cost is justified by a measured, real pain point rather than a hypothetical one.
- REST's stable, resource-based URLs (
GET /orders/123) map directly onto standard HTTP caching headers (Cache-Control,ETag) that browsers and CDNs already understand; GraphQL typically uses one single endpoint whose response shape varies per query, so the same simple URL-based caching rule does not apply and a dedicated client-side cache library is needed instead. - Supabase Realtime, because it is already part of the stack (built on Postgres logical replication plus WebSockets under the hood) and requires no additional server, message broker, or custom WebSocket implementation to notify the dashboard the instant a row changes.
Further reading
- MDN: An overview of HTTP: https://developer.mozilla.org/en-US/docs/Web/HTTP/Overview
- MDN: REST glossary entry: https://developer.mozilla.org/en-US/docs/Glossary/REST
- GraphQL official docs: https://graphql.org/learn/
- Apollo GraphQL docs: https://www.apollographql.com/docs/
- gRPC official docs: https://grpc.io/docs/
- gRPC-Web project docs: https://github.com/grpc/grpc-web
- Protocol Buffers docs: https://protobuf.dev/
- MDN: Server-Sent Events: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events
- MDN: WebSockets API: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API
- Supabase Realtime docs: https://supabase.com/docs/guides/realtime
- Supabase Row Level Security docs: https://supabase.com/docs/guides/database/postgres/row-level-security
- Supabase PostgREST docs (how Supabase's REST layer works): https://docs.postgrest.org/
- Next.js Route Handlers docs: https://nextjs.org/docs/app/building-your-application/routing/route-handlers
- MDN: Guide to HTTP caching: https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Caching
- MDN: Fetch API (used for REST calls from the client): https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
- MDN: HTTP response status codes: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
- OpenAPI Specification docs: https://swagger.io/specification/
- gRPC status codes reference: https://grpc.io/docs/guides/status-codes/
- Postgres logical replication docs (what Supabase Realtime is built on): https://www.postgresql.org/docs/current/logical-replication.html
- Google Cloud API design guide (official docs): https://cloud.google.com/apis/design
- GraphQL: thinking in graphs (official schema design guide): https://graphql.org/learn/thinking-in-graphs/
- Next.js Middleware docs (relevant to REST auth checks): https://nextjs.org/docs/app/building-your-application/routing/middleware