Saltar al contenido principal

ADR-010 — @tedos/core: build our own Refine-inspired app framework

:::note Contenido en inglés Esta página del wiki se sincroniza desde la base de conocimiento en inglés y todavía no está traducida. :::

canonical · tech-lead · updated 2026-06-27 · source

  • Status: Accepted
  • Date: 2026-06-27
  • Owner: Tech Lead
  • Related issue: #934 (effort) · scaffold sub #935 · provider subs #936–941 · runtime tenant config #922

Note: the intended slug was "ADR-009"; ADR-009 was already taken by ADR-009-crm-via-twenty.md. This decision is recorded as ADR-010 (next free number — latest-number-wins).

Context

  • Every Next.js app in the fleet (apps/admin, apps/console, apps/clients/*) re-implements the same plumbing: auth gating, a data-fetch seam, routing/resource inference, i18n, and per-tenant config. ADR-008 (thin apps over @tedos/* packages) says this belongs in a package, not copied into each app.
  • Refine (@refinedev/core) is the reference framework for exactly this pattern — a <Refine> shell that composes pluggable providers (authProvider, dataProvider, routerProvider, accessControlProvider, i18nProvider) over a resources registry. Its concepts fit us; its runtime does not.
  • Forces: our stack is Next 16 / React 19, server-first RSC (ADR-008). Refine is a client-runtime framework (its core is built around client hooks/context and TanStack Query); adopting it would push a "use client" boundary high in the tree and fight RSC. It is also a heavyweight third-party dependency to take on a load-bearing seam.
  • We need ONE <TedosApp> shell every app mounts once, inheriting providers + a resource registry + the runtime per-tenant config that effort #922 produces (enabled modules/resources, branding tokens, locale, auth org).

Decision

Build our own RSC-first, Refine-inspired core (@tedos/core) — do NOT adopt @refinedev.

  • A new package @tedos/core owns: the typed provider contracts (the seam), the <TedosApp> shell (a server-safe entry + a single "use client" provider island holding the React context), and the consumer hooks (useTedos() + per-provider hooks).

  • Refine's concepts map onto our existing packages — we don't reinvent the implementations, we wrap what we already have:

    Refine concept@tedos/core seamBacked by (impl, subs #936–941)
    dataProviderDataProvider@tedos/api-client (the ONE typed transport, ADR-008 BD)
    authProviderAuthProvider@tedos/auth / Clerk (auth stays app-owned, ADR-008 A1)
    accessControlProviderAccessControlProviderreads AuthProvider.getPermissions
    routerProviderRouterProviderNext.js App Router (next/navigation + route params)
    i18nProviderI18nProvidernext-intl (added by the i18n sub, not the scaffold)
    resourcesResource[] registryapp-owned hrefs/segments + display meta (ADR-008 A3)
  • Resource concept: a Resource maps a domain entity (name) to its app-owned route templates (list/show/create/edit) + display meta (label, icon, parent). The router infers the active resource/action/id from the URL; the data layer keys calls by name; nav reads meta. Routing stays app-owned (no hardcoded paths in the package).

  • Runtime tenant config (#922): <TedosApp> accepts a TenantConfigenabledModules, resources, brandingTokens (white-label CSS var overrides, color-scope.md), locale, authOrgId. It is sourced at runtime from effort #922; @tedos/core only types it and threads it through context.

  • RSC discipline (ADR-008 B1): the <TedosApp> server entry is "use client"-free and can mount in a Server Component root layout; the only client boundary is the leaf provider island that holds the React context.

  • Scope boundaries: this decision (and the scaffold sub #935) cover the package + the shell + the typed contracts + this ADR ONLY. The concrete provider implementations are sibling subs (#936–941) built against these contracts. No state-management opinion is added here — ADR-003 (Zustand + TanStack Query) is unchanged; client data hooks, if any, are a provider-impl concern.

Consequences

Positive

  • Server-first by construction — no client framework runtime fighting RSC; the client boundary is a single leaf.
  • No heavyweight third-party dependency on a load-bearing seam; we own the surface and can evolve it with our stack.
  • Apps converge on ONE shell + ONE set of provider contracts — the thin-app architecture (ADR-008) realized for cross-cutting app concerns, not just UI.
  • Providers wrap packages we already maintain (@tedos/api-client, @tedos/auth), so there is one implementation per concern (no duplication).

Negative / trade-offs

  • We maintain the framework ourselves — Refine's ecosystem (its data-provider adapters, hooks, inferencer, devtools) is not available to us.
  • The provider contracts are ours to keep stable; a breaking change ripples to every consuming app.
  • Passing provider objects (which hold methods) across the server→client boundary needs care; the exact construction strategy per provider is finalized in the impl subs against these contracts.

Follow-ups required

  • Subs #936–941: implement AuthProvider (@tedos/auth/Clerk), AccessControlProvider, RouterProvider (Next App Router), DataProvider (@tedos/api-client), I18nProvider (next-intl) against the @tedos/core/providers contracts.
  • Effort #922: produce the runtime TenantConfig and wire it into each app's <TedosApp> mount.
  • Apps adopt <TedosApp> in their root layout once the providers land (separate effort).

Alternatives considered

OptionWhy not
Adopt @refinedev/coreClient-runtime framework (client hooks + TanStack Query core); fights RSC (ADR-008), heavyweight dependency on a load-bearing seam.
No framework — each app wires its ownViolates ADR-008 (thin apps); duplicates auth/data/router/i18n plumbing across apps; the twins we just deleted.
A loose set of hooks, no <TedosApp>No single mount point for providers + tenant config; every app re-composes the same boilerplate.

References