FeatherNeko owns very little of the data it presents. Anime metadata comes from external APIs, streaming information comes from another provider, and availability can change without warning. Responses can be slow, stale, incomplete, rate-limited, or temporarily unavailable. There is no private application backend sitting between those services and the client to normalize their behavior before the UI sees it.
From the user's perspective, however, none of that should matter.
FeatherNeko is an Android anime discovery and streaming client built with React Native. It lets users browse shows, search for titles, inspect episodes, maintain a watchlist, resume playback, track viewing history, and stream through an external provider. The feature set is intentionally focused: there are no microservices behind it, no private application database, and no backend invented simply to make the architecture appear larger than the product needs.
That constraint is what made the project technically interesting. Without a server acting as the application's source of truth, the frontend has to decide what truth means for itself. Some state needs to survive an app restart. Some only matters during the current navigation flow. Some remote data can be reused for a long time, while other responses should expire quickly. Some stale data is still useful during an upstream failure; some should be discarded immediately.
FeatherNeko therefore became less about rendering API responses and more about coordinating them. The client has to reconcile remote APIs, cached responses, persisted user state, playback state, and whatever the upstream providers currently believe to be true. The project became an exercise in creating continuity on top of systems I do not control.
A focused product with a surprisingly large client responsibility
The repository contains one application rather than a monorepo, and the product stays centered on four main flows: discovery, detail, playback, and personalization.
A user lands on a home feed, browses trending or seasonal titles, opens an anime, inspects metadata and episodes, and moves into playback. Around that primary flow, FeatherNeko maintains watchlist membership, viewing history, continue-watching state, playback progress, preferences, and the currently selected anime or episode.

The home screen is a useful illustration of what the architecture is trying to protect. What the user sees is straightforward: a featured title, horizontally browsable media, progress-aware content, and familiar navigation. Underneath that interface, however, the app is deciding what can come from persisted local state, what requires network data, what can be served from cache, and which responses should quietly prepare data for a later screen.
There is also a local password gate, but no conventional server-owned authentication session. FeatherNeko is fundamentally client-driven. That means reliability work that might normally be divided between a frontend and backend lands much closer to the UI runtime itself.
If the home feed should open quickly, the client has to make that happen. If a detail page should still be useful while an upstream source is rate-limited, the client has to decide how to degrade. If selecting "continue watching" should return to the right episode and progress point, that continuity has to exist in local application state.
The product asks for continuity. The architecture has to create it.
Treating the frontend as a data system
The architectural decision that shaped the project was to stop treating the app as a thin shell over remote APIs.
FeatherNeko instead treats the client as a small data system with distinct persistence and ownership rules. It uses a policy-driven cache rather than one undifferentiated object store, composes information from multiple upstream services, and separates durable user state from transient navigation state and API-shaped data.
At a high level, the runtime looks like this:
The point of this architecture is not the number of boxes. It is that each layer is responsible for a different kind of memory.
Redux with redux-persist holds state that should remain meaningful across application restarts: watchlist data, viewing history, playback progress, settings, and local authentication state. React Context handles flow-specific selection such as the anime and episode currently being viewed. The cache stores responses shaped by remote data and applies expiration and revalidation policy. A local entity layer helps reconcile anime and episode information that arrives from different sources.
Those distinctions prevent every kind of state from drifting into one global store merely because it is convenient.
State ownership matters more than the state library
One of the more useful lessons from FeatherNeko was that choosing Redux or Context is less important than deciding what a piece of state represents.
Playback progress belongs in durable state because it should survive navigation and application restarts. The currently selected episode is different: it belongs to the immediate user flow and does not automatically need global persistence. API responses are different again. They may need caching, expiration, revalidation, or replacement without becoming part of the user's durable application state.
That distinction shapes the startup sequence as well. The root layout mounts Redux, its persistence gate, safe-area and theme providers, and the application routes. Persisted state has to rehydrate before authentication redirection is meaningful. Cache cleanup runs when the application becomes active, while cache migration is performed lazily rather than turning application startup into one large blocking maintenance phase.
Even the bootstrap path therefore balances correctness against latency. The app needs enough state to reconstruct a coherent session without making every launch pay for work that can safely happen later.
The cache model became one of the most important parts of the app
If there is one part of FeatherNeko that best captures the engineering direction of the project, it is the cache architecture.
The application does not put every external response into one cache with one expiration rule. It uses policy-driven namespaces so feeds, anime metadata, streaming lookups, and other remote data can have different lifecycles. Cache policy can account for TTL, persistence tier, revalidation behavior, source health, and whether stale data remains useful when fresh data cannot be retrieved.
That matters because anime data does not all change at the same speed.
Titles, artwork, and core descriptors tend to be relatively stable. Airing information and other fast-changing properties are more volatile. The application therefore separates stable and volatile media data rather than forcing both through the same invalidation policy. Stable data can continue providing value without being repeatedly discarded just because another part of the record changes more frequently.
The same principle appears in the home feed. The app checks its feed namespace first. If the entry is missing or no longer usable, it fetches from AniList, accounts for rate limits and transient failure, and writes the result back into cache. That request can also seed anime entities that later screens can reuse.
The home screen is therefore doing more than rendering cards. It is prewarming the path into the rest of the application.
Detail pages follow another read path. The app can merge stable and volatile cached segments, render what it already has when sufficient detail is available, and fetch fresh information when required. Episode handling introduces Kitsu data, local entities, page-level caching, and repair logic because "fetch the episodes" is not enough when several sources need to agree on which episode is which.
This is also where stale fallback becomes valuable. If an upstream source is temporarily rate-limited or unavailable, returning a recently cached response can be a better product decision than converting a temporary provider problem into a completely broken screen.
For an application built largely on third-party data, graceful degradation is part of the product.
Playback exposes every state boundary at once
The watch flow is where those design decisions stop being abstract.
By the time the user reaches an episode, FeatherNeko may need the selected anime, selected episode, locally persisted progress, viewing history, provider mapping, cached metadata, stream-source information, request headers, and route state. It also has to support entry through more than one path: a direct tap from an episode list is not necessarily the same runtime situation as resuming through persisted continue-watching state.
The sequence looks roughly like this:
None of those operations is especially exotic in isolation. The complexity comes from their interaction.
Provider resolution benefits from caching because repeating a search against an upstream service is both slower and less reliable. Playback progress should not live only in transient component state because it needs to survive. Current selection does not necessarily belong permanently in Redux because it is contextual to the active flow. Stream responses have different lifetimes from anime metadata and may require provider-specific handling.
A single "play episode" interaction therefore crosses several types of state ownership.
That is the point where a frontend can quietly stop behaving like a view layer and start behaving like a coordinator.
Streaming and discussion are part of one viewing flow
The playback interface also has to leave room for the media experience around the video itself.

The screen combines an episode preview with discussion content underneath it. From a product perspective, that means playback cannot be treated as an isolated video component. The app needs enough context to know which anime and episode are active while keeping persistent viewing state separate from transient UI state.
The screenshot also reflects one of the tensions in the implementation: the watch screen became the most complex file in the repository. By the time a user reaches it, the screen is composing provider resolution, source fetching, progress updates, continue-watching state, current selection, route-based resume behavior, and the interface around playback.
That concentration of responsibility is not something I would present as an ideal end state. It is more useful as evidence of where the architecture began to strain.
The original separation still makes sense: durable user state in Redux, transient selection in Context, remote data in cache and local entities. But once one screen is responsible for coordinating all of those layers, that screen begins acting as a workflow engine as much as a view.
Recognizing that boundary is more useful than simply labeling the file "too large." A large file can be a symptom of poor decomposition, but it can also reveal where a real product workflow has accumulated enough responsibility to deserve its own abstraction.
Working with several sources without pretending they are one API
FeatherNeko composes AniList, Kitsu, AnimeAPI, and Jikan/MAL rather than forcing one provider to solve every product requirement.
AniList supplies GraphQL-driven anime data used by discovery flows. Other sources fill different roles in metadata, episodes, or streaming. The client has to reconcile those sources without assuming that identifiers, freshness, reliability, or response shape are naturally aligned.
That introduces an important constraint: upstream APIs remain external systems. FeatherNeko cannot control their latency, availability, rate limits, or future behavior.
The app therefore benefits from isolating those dependencies behind read paths and cache policy instead of allowing screens to make arbitrary network requests directly. This gives the runtime somewhere to express behaviors such as retries, cached fallback, source-specific data handling, and normalized local records.
It also makes the project more honest. FeatherNeko does not own the upstream truth. It owns how responsibly it consumes that truth.
Where the architecture worked—and where it started to strain
The strongest part of the architecture is the separation of state by lifecycle.
Persistent user behavior belongs in durable Redux state. Temporary selection belongs closer to the flow. External data belongs behind caching and reconciliation rules. Stable and volatile media data can expire independently. Multiple APIs are treated as separate systems rather than magically equivalent endpoints.
Those decisions make the application more resilient without requiring a backend of its own.
The primary tension is coordination complexity. As more responsibility moves into the client, individual screens can become responsible for combining too many layers at once. The watch flow demonstrates that most clearly. It has good underlying primitives, but it also reveals the limit of letting a feature screen perform orchestration directly.
There is a broader tradeoff here as well. A client-only architecture removes a backend deployment and keeps the project relatively lightweight, but it also means there is no server-owned normalization layer or canonical private application database. Reliability improvements therefore tend to increase local client sophistication.
That was an acceptable tradeoff for this project's scope, but it is still a tradeoff rather than a free simplification.
What FeatherNeko taught me
The most valuable lesson from FeatherNeko was that architectural seriousness is not reserved for large backend systems.
A single mobile application can still have meaningful questions about consistency, durability, caching, data ownership, graceful degradation, and recovery. In some ways those decisions become more visible in a client-only architecture because there is no backend where difficult state problems can quietly be pushed.
The project also changed how I think about "frontend state." That phrase can make very different data sound equivalent. A theme preference, a selected episode, an AniList response, playback progress, and a provider mapping are all technically state, but they do not have the same meaning, lifetime, or reliability requirements. Treating them as if they do creates accidental complexity later.
FeatherNeko became much easier to reason about once those differences were explicit.
It also reinforced the value of caching as a product tool rather than merely a performance optimization. The right cache policy can preserve continuity through slow responses, rate limits, temporary provider failures, and navigation between screens. In that sense, reliability is partly about deciding what the application is allowed to remember.
Takeaway
FeatherNeko began as an anime client, and it remains intentionally smaller in scope than many of my other projects. That is precisely why its architecture became useful to study.
There is no elaborate backend hiding the hard parts. The client itself has to decide what should persist, what should expire, what can be reconstructed, how external sources should be combined, and how playback state should remain coherent as the user moves through the application.
The result is not that FeatherNeko somehow stopped being a frontend. It is that "frontend" stopped being an adequate description of its responsibility.
Once an application owns persistence boundaries, cache policy, data reconciliation, recovery behavior, and playback continuity across several external systems, rendering is only part of the job. The client becomes the place where those independent systems are assembled into one product experience.
That is the part of FeatherNeko I find most technically meaningful.
