ADR-0005: Use Refine for Frontend (Studio & Desk)
- Status: Proposed
- Date: 2026-01-07
- Deciders: @anshpansuriya14
- Supersedes: N/A
- Superseded by: N/A
Context
Framework M requires a frontend solution for two distinct components:
| Component | Purpose | Phase |
|---|---|---|
| Studio | Visual DocType builder that generates Python code | Phase 07 |
| Desk | Generic admin UI rendering forms/lists from metadata | Phase 09 |
Forces at play:
- Architectural Alignment: Frontend must follow our Ports & Adapters pattern (ARCHITECTURE.md Sec 3.2)
- Metadata-Driven: Backend generates JSON Schema, frontend renders forms dynamically (Sec 5.4)
- UI Flexibility: Support for "Progressive Customization" — Indie (default UI) → Plugin (extend) → Eject (replace)
- Stack Preference: Phase 09 specifies Tailwind CSS + shadcn/ui as the UI layer
- No Vendor Lock-in: Must be fully open-source with no paid enterprise tier for core features
- TypeScript-first: Type safety is a core principle (CONTRIBUTING.md)
Frameworks Evaluated:
| Framework | Stars | License | Focus |
|---|---|---|---|
| Refine | 30k+ | MIT | Headless CRUD meta-framework |
| React Admin | 25k+ | MIT (Enterprise paid) | Material UI admin framework |
| AdminJS | 8k+ | MIT | Node.js backend admin generator |
| Tremor | 17k+ | Apache 2.0 | Dashboard/analytics components |
| RJSF | 14k+ | BSD-3 | JSON Schema form renderer |
Decision
We will use Refine as the primary frontend framework for Studio and Desk, with shadcn/ui for components, RJSF for schema-driven forms, and Tremor for dashboards.
Full Stack:
| Layer | Technology | Purpose |
|---|---|---|
| Data/State | Refine Core | CRUD, caching, auth, access control |
| Forms | RJSF + react-hook-form | Schema-driven form rendering |
| Tables | @tanstack/react-table | Flexible data tables |
| UI Components | shadcn/ui | Accessible, Tailwind-based components |
| Styling | Tailwind CSS | Utility-first styling |
| Charts | Tremor | Dashboard analytics |
| Code Editor | Monaco Editor | Studio code preview |
| Drag & Drop | @dnd-kit | Studio field editor |
Deployment Configuration (Base URLs):
Different deployments require different URL configurations:
| Scenario | UI Base | API Base | Config |
|---|---|---|---|
| Same Origin (Default) | / | /api/v1 | No config needed |
| Subdirectory | /portal | /api | API_BASE_URL=/api |
| Separate Subdomains | portal.example.com | api.example.com | API_BASE_URL=https://api.example.com |
| CDN for UI | cdn.example.com/app | api.example.com | Full URL in config |
Configuration via window.__FRAMEWORK_CONFIG__ or environment:
// Injected by backend or build-time env
window.__FRAMEWORK_CONFIG__ = {
apiBaseUrl: "/api/v1", // Default: same origin
metaBaseUrl: "/api/meta", // Metadata endpoint base
wsBaseUrl: "/api/v1/stream", // WebSocket base
};
Data provider uses configured base URL:
const config = window.__FRAMEWORK_CONFIG__ || { apiBaseUrl: "/api/v1" };
const dataProvider = frameworkMDataProvider(config.apiBaseUrl);
// fetch(`${config.apiBaseUrl}/${resource}`) → configurable
Why Refine aligns with Framework M:
Framework M (Python) Refine (TypeScript)
─────────────────────────────────────────────────────
RepositoryProtocol ←→ DataProvider
PermissionProtocol ←→ AccessControlProvider
AuthContextProtocol ←→ AuthProvider
EventBusProtocol ←→ LiveProvider
DocType ←→ Resource
BaseController ←→ Resource hooks
Consequences
Positive
- Architectural Symmetry: Refine's Provider pattern is identical to our Ports & Adapters
- Headless Design: Enables all three customization modes (Indie, Plugin, Eject)
- Metadata-Driven: Native
useForm+ RJSF integration forGET /api/meta/{doctype} - No Vendor Lock-in: MIT license, all features are free
- Stack Compatibility: Works seamlessly with Tailwind + shadcn/ui
- Active Community: 30k+ stars, excellent documentation, regular releases
Negative
- Custom Data Provider: Requires ~100 lines of boilerplate (well-documented pattern)
- No Built-in Guessers: Unlike React Admin, no auto-generated UI (mitigated by RJSF)
- Learning Curve: Team needs to learn Refine's hook-based API
Neutral
- TypeScript-first (aligns with our preference)
- React Query under the hood for caching
Alternatives Considered
| Option | Pros | Cons |
|---|---|---|
| Chosen: Refine | Headless, provider pattern matches our architecture, MIT, works with any UI | Requires custom data provider |
| React Admin | Fast to start, guessers auto-generate UI, large ecosystem | Material UI lock-in, paid enterprise features, conflicts with Tailwind |
| AdminJS | Auto-generates admin from ORM | Node.js only — incompatible with Python/Litestar |
| Tremor | Excellent charts, Tailwind-native | Dashboard-focused, not a complete admin framework |
| RJSF | Perfect for JSON Schema forms | Form library only, not a full stack |
Why React Admin was rejected:
| Framework M Principle | React Admin Conflict |
|---|---|
| UI-agnostic (Sec 2) | Locked to Material UI |
| Tailwind/shadcn stack (Phase 09) | Material UI incompatible |
| No vendor lock-in | Enterprise features (RBAC, audit) are paid |
| Additive Design | Difficult to swap UI layer without full rewrite |
Why AdminJS was rejected:
- Designed exclusively for Node.js backends (Express, NestJS, Fastify)
- Requires tight ORM integration (TypeORM, Prisma)
- Does not work with Python/Litestar
References
- Refine Documentation
- React Admin Documentation
- RJSF Documentation
- Tremor Documentation
- Related: ADR-0001, ADR-0002
- Phase Checklists: Phase 07, Phase 09