Frontend architecture
Syncturtle’s frontend is built in a Yarn + Turborepo monorepo with multiple Next.js apps and shared packages. This page focuses on the runtime UI architecture: how state flows through the app, why the store pattern exists, and how i18n is designed.
If you’re reviewing the system: the goal is readable, testable, low-magic frontned engineering that scales with complexity.
Apps and packages
Apps
apps/web/- the end-user application (the “Life OS” UI)apps/admin/- instance administration and governance UIapps/docs/- documentation site (Nextra / MDX)
Shared packages (workspace deps)
@syncturtle/ui- reusable UI components@syncturtle/constants- shared constants and config defaults@syncturtle/utils- utilties (including theExternalStorebase)@syncturtle/i18n- translations, provider, anduseTranslation()hook
This keeps the apps thin and the shared logic reusable without copy/paste.
UI composition and layouts
Each app follows a predictable layering:
app/(routes and layout) - Next.js App Routercore/(cross-cutting UI primitives) - header, footer, providers, wrappersservices/(API client code) - fetchers / transportstore/(state) - class-based stores and derived snapshotshooks/(ergonomics) -useInstance(),useUser(),useAppTheme(), etc.
In practice, you should be able to open any route and quickly trace: page -> component -> hook -> store action -> service call -> API.
Why class stores (instead of MobX / Redux)
Syncturtle uses vanilla class stores (a “RootStore” composed of feature stores) for a few reasons:
-
Explicitness over magic: MobX is productive, but it can hide the “why” behind updates (what triggers renders, how subscriptions work, what is reactive, etc.).
-
React-native integration: React’s recommended pattern for external sources is
useSyncExternalStore, which aligns well with a store that maintains a snapshot and a subscription mechanism. -
Testability and portability: Stores are plain classes: no reducers, no middleware, no global singleton requirements. You can test them like normal code (instantiate, call action, assert snapshot).
-
DX (dveloper experience) The API is ergonomic:
const {fetchInstanceInfo, instance, isLoading} = useInstance();- actions are methods, not dispatch strings
- derived properties and orchestration live in the store, not the component tree
useSyncExternalStore: the core idea
React needs a safe way to subscribe to data sources outside React state. useSyncExternalStore does exactly that:
- You provide:
subscribe(listener)- how React knows when to re-rendergetSnapshot()- the current immutable view of stategetServerSnapshot()- for SSR consistency
In Syncturtle, each store exposes internal methods like:
_subscribe_getSnapshot_getServerSnapshot
and hooks wire those into React.
What “snapshot-based” state means
Each store owns an immutable snapshot object ({ ... }). Updates replace the snapshot (or patch it) and emit a single “change” signal.
React reads the snapshot, and your components render based on plain data.
This avoids accidental mutation bugs and makes it obvious what changed.
Useful info here
Store structure: RootStore + feature stores
The pattern is:
RootStorecomposes features stores:theme,router,instance,user, etc.- Each feature store encapsulates:
- a snapshot type (data + loading + error)
- actions that mutate snapshot
- service calls (or delegates to a
Serviceclass)
Example (conceptual):
class RootStore {
instance = new InstanceStore();
user = new UserStore(this);
}This mirrors the “RootStore + child stores” style you liked in MobX, but with fully explicit mechanics.
Hooks: ergonomic access to stores
Hooks exists to make UI code clean and intention-revealing.
Example usage:
const { isLoading, instance, fetchInstanceInfo } = useInstance();