Open-source project
nkzw-tech/fate avatar
nkzw-tech/fate

fate: Relay's data model without GraphQL on the server

A modern data client for the web.

1,128 stars22 forksTypeScriptMIT

At a glance

What is it?
fate is a TypeScript data client that takes Relay's ideas, co-located fragments, a normalized cache and a compiler that hoists requests, and applies them to plain TypeScript with a native protocol and optional tRPC adapter. The result is a data layer with a small API surface, but it asks you to change how you structure a React or Vue application.
Who is it for?
fate suits a team that wants Relay's ergonomics, co-located data requirements, predictable caching and declarative optimistic updates, but does not want to put a GraphQL server and type system in the backend, because fate keeps the client-side data model and speaks to your existing API.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 2 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Why not just adopt GraphQL and Relay

The README's central argument is about cost rather than capability, and it starts from the team's own practice.

Nakazawa Tech builds apps and games primarily with GraphQL and Relay, and advocates for those technologies in talks, providing server and client templates to help developers get started. But GraphQL comes with its own type system and query language. If you are already using tRPC or another type-safe RPC framework, it is a significant investment to adopt and implement GraphQL on the backend. This investment often prevents teams from adopting Relay on the frontend.

So the gap fate targets is specific. The people who want fragment composition, co-located data requirements, predictable caching and deep integration with modern React features, but who are not willing to run a GraphQL server.

Many React data frameworks lack Relay's ergonomics, especially fragment composition, co-located data requirements, predictable caching, and deep integration with modern React features. Optimistic updates usually require manually managing keys and imperative data updates, which is error-prone and tedious.

What fate offers in exchange is stated in the README as view composition, normalized caching, data masking, Async React features, and type-safe data fetching, with a minimal API, no DSL, and no magic, it is just JavaScript.

The project is MIT licensed, the homepage is fate.technology, the primary language is TypeScript, and the topic list names async, data and react, with the framework spreading to Vue as well.

Views, composed upward into one request

The core primitive is the view, and the example in the README is short enough to take in at a glance.

tsx
export const PostView = view<Post>()({
  author: UserView,
  content: true,
  id: true,
  title: true,
});

export const PostCard = ({ post: postRef }: { post: ViewRef<'Post'> }) => {
  const post = useView(PostView, postRef);

  return (
    <Card>
      <h2>{post.title}</h2>
      <p>{post.content}</p>
      <UserCard user={post.author} />
    </Card>
  );
};

A component declares what it needs with a view object. The keys are the fields to select, and the values are either true for a scalar or another view for a relation, as author is UserView here. The component then receives a ViewRef and calls useView to read the data.

The mechanism the README describes is that views are composed upward through the component tree until they reach a root, where the actual request is made, and fate fetches all required data in a single request. That is the compiler hoisting idea from Relay, and it is what eliminates network waterfalls.

Loading and error handling follow from the same design. React Suspense manages loading states, and any data-fetching errors naturally bubble up to React error boundaries. This eliminates the need for imperative loading logic or manual error handling.

So a component does not fetch, does not track a loading boolean, and does not catch an error. It declares a shape, and the framework works out the request and the lifecycle. That is the whole ergonomic argument, and the code above is its clearest statement.

Data masking and strict selection as a design constraint

The normalized cache is standard Relay territory, and the README describes it as maintaining a normalized cache for all fetched data, which enables efficient data updates through actions and mutations and avoids stale or duplicated data.

The interesting part is the second half of that feature, data masking and strict selection. fate enforces strict data selection for each view, and masks, hides, data that components did not request. This prevents accidental coupling between components and reduces overfetching.

That is a stronger constraint than most data libraries impose. The usual model is that a component can read any field the server happened to return. Here, a component that did not declare a field cannot read it, and the server is expected to respect the selection. Two components asking for overlapping data get the same normalised entry, and a component that wants a field it did not declare has to declare it.

The rationale is stated plainly: preventing accidental coupling between components. Without it, a component quietly depends on a field that another component happened to request, and when that other component changes its selection, the first one breaks. The masking makes the dependency explicit at the point where it is written.

The cost is that the selection set is now part of your API surface, and changing what a component can read is a change to that surface. For a team that has been bitten by implicit data coupling, that is the feature. For a team that prefers maximum flexibility, it will feel like a straitjacket, and the README's AI-Ready claim is the tell that this is deliberate. A minimal, predictable API and explicit data selection enable local reasoning, enabling humans and AI tools to generate stable, type-safe data-fetching code.

Optimistic updates, live views and pagination

Three of the features are worth separating out because each solves a different class of problem.

Optimistic updates: fate supports declarative optimistic updates for mutations, allowing the UI to update immediately while the server request is in-flight. If the request fails, the cache and its associated views are rolled back to their previous state. The key word is declarative. The README's criticism of other frameworks is that optimistic updates usually require manually managing keys and imperative data updates, so the rollback is a consequence of the model rather than a block of cleanup code you have to write.

Live Views: fate can keep individual view refs up to date through a single native Server-Sent Events stream, merging updates into the normalized cache. A single stream feeding the cache rather than per-component subscriptions is the interesting part, since it means the subscription cost does not grow with the number of views on screen.

Lists and pagination: fate provides built-in support for connection-style lists with cursor-based pagination, making it easy to implement infinite scrolling and load-more functionality. Cursor-based pagination in particular is what you want for lists that get appended to while the user is scrolling, since offset pagination produces duplicates and gaps in exactly that situation.

The Async React line is the thread running through all of them. fate uses modern Async React features like Actions, Suspense, and use to support concurrent rendering and enable a seamless user experience. Actions, in particular, are what make a mutation a unit of work the framework can roll back.

Install paths: template, React package, Vue package, server package

There are three ways in, and the template route is the one the README leads with.

Create a new fate app from a template with Vite+, using the vp create fate command.

bash
vp create fate

The template selector can create a React or Vue client for a Void app with Drizzle, a tRPC app with Drizzle or Prisma, a GraphQL app with Prisma, or a fate client for an existing GraphQL server. React is the default UI framework, and you pass --framework vue or choose Vue in the selector for a Vue app. The template sources live in the fate repository under packages/create-fate/templates/fate.

The manual route installs the client package. For a React client, install react-fate, which requires React 19.2 or newer.

bash
npm add react-fate

For a Vue client, install vue-fate, with the same three package managers available.

bash
npm add vue-fate

If your server is a separate package, install @nkzw/fate there as a runtime dependency too. Install @nkzw/fate on the client only for a barebones integration without a framework adapter.

bash
npm add @nkzw/fate

That last distinction is the architecture in one sentence. The core package is framework-agnostic, the react-fate and vue-fate packages are the framework adapters, and the server uses the core package directly.

The example/ directory in the repository mirrors the template options, with client-vue/, client/, persistence/, server-drizzle/, server-graphql/, server-prisma/ and void/ directories, plus a seedData.ts, so you can read a working configuration for each combination.

A monorepo with its own docs pipeline

The repository layout shows the scope of the project. There is a packages/ directory holding the core and the framework packages, an example/ directory with the working applications, a docs/ directory, a.vitepress/ directory, and a scripts/ directory. There is also an AGENTS.md at the root, a CONTRIBUTING.md, a typedoc.json, a vite.config.ts, a pnpm-workspace.yaml and a pnpm-lock.yaml.

The pnpm workspace plus the.vite-hooks directory and the vp command used in the templates indicate that the project has its own build tool. The root package.json scripts show the shape: build:all runs vp run -r build followed by generate-readme and copy-files, and the dev scripts are per-example.

The copy-files script is revealing. It copies the README.md and LICENSE into packages/fate, packages/react-fate, packages/vue-fate and packages/fate-indexeddb, and copies the LICENSE into packages/void-fate. Each published package carries its own README and licence, which is what npm shows on the package page.

The README itself carries a comment saying it is auto-generated from docs/guide/*.md and docs/integrations/*.md and should not be edited directly. The documentation is a VitePress site built from the same source, with typedoc generating the API reference, and the build:docs script runs the API docs build, copies the per-package docs, and then runs vitepress build.

The docker-compose.yml in the root is minimal. One service, db, running postgres:17 with a restart policy, publishing 5432, with POSTGRES_USER set to fate and POSTGRES_PASSWORD set to echo. It exists so the examples can run against a local database.

Version 1.8.0 and what the project is still deciding

The release history is v1.8.0 on 2026-10-15, v1.7.7 and v1.7.6 both on 2026-10-05, and the last push was on 2026-10-15, matching the newest tag. The project is past 1.0, so the API surface is meant to be stable, and the two releases on the same day in October 2026 suggest a patch and a follow-up correction landing together.

The scope is still expanding, though, and the README's own roadmap is a good place to see it. fate is described as designed to make data fetching and state management in React applications more composable, declarative, and predictable, and the API is aimed at reducing data fetching complexity.

Two things stand out as things to check before adopting rather than after. The first is that the framework choice is made at project creation, since the template selector produces either a React or a Vue client, and the README says React is the default with --framework vue as the alternative. The packages are separate, react-fate and vue-fate, so a project could in principle use both, but the template does not.

The second is the transport. fate uses a native protocol with optional adapters such as tRPC, and the repository carries example/server-drizzle/, example/server-prisma/, example/server-graphql/ and example/void/ alongside a dev:client:graphql script that sets VITE_FATE_TRANSPORT=graphql. The word optional is doing work in the README, and the fact that there is a transport environment variable means the client and server can disagree about the protocol unless you are deliberate about it.

Editorial conclusion

fate suits a team that wants Relay's ergonomics, co-located data requirements, predictable caching and declarative optimistic updates, but does not want to put a GraphQL server and type system in the backend, because fate keeps the client-side data model and speaks to your existing API. It is the wrong tool for a project with no backend at all, since a server or adapter is still required, and it is the wrong choice if you want zero framework coupling, since the view primitive is the whole idea. Before you commit, read docs/guide/why-fate.md, and try the template selector's React and Vue options, because the framework choice is made at project creation and the README notes the template sources live inside this repository.

Frequently asked questions

What is fate and what problem does it solve?

fate is a modern data client for the web inspired by Relay and GraphQL, which takes Relay's ideas such as co-located data requirements, a normalized cache and hoisted requests and applies them to plain TypeScript data fetching, with a native protocol and optional adapters such as tRPC.

How do I install fate in an existing React project?

Run npm add react-fate, or the pnpm or yarn equivalent. The React client requires React 19.2 or newer, and for a Vue client you install vue-fate instead.

What does data masking mean in fate?

fate enforces strict data selection for each view and hides data that components did not request, which prevents accidental coupling between components and reduces overfetching. A component that does not declare a field cannot read it.

How does fate handle a failed optimistic update?

The UI updates immediately while the server request is in-flight, and if the request fails the cache and its associated views are rolled back to their previous state.

What is the Void app in the fate template selector?

The template selector can create a React or Vue client for a Void app with Drizzle, a tRPC app with Drizzle or Prisma, a GraphQL app with Prisma, or a fate client for an existing GraphQL server. The template sources live under packages/create-fate/templates/fate in the repository.

Official sources

  1. License: MIT
  2. nkzw-tech/fate on GitHub
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/nkzw-tech-fate.svg)](https://hysenlabs.com/projects/nkzw-tech-fate)

Was this useful?