Frontend Customization Guide
This guide explains how to work with Framework M's bundled Desk UI and customize it for your needs.
Overview
Framework M ships with a pre-built Desk UI (similar to Frappe's Desk), providing a batteries-included experience. You can:
- Use the bundled UI - Zero npm setup required
- Customize the bundled UI - Run Vite dev server for live development
- Create custom frontends - Scaffold new UIs from templates
Quick Start: Bundled Desk UI
No npm Required
The bundled Desk UI is included in the Python package. Just install and run:
# Install Framework M
pip install framework-m
# Start the application
m start
That's it! Open http://127.0.0.1:8000 in your browser. The Desk UI is pre-built and served automatically.
How It Works
- Pre-built static files are bundled in the PyPI package at install time
- Served automatically at
/desk/by the backend - No build step needed - ready to use immediately
- SPA routing - Client-side navigation works out of the box
This approach mirrors Frappe's bench start experience - instant productivity without frontend tooling complexity.
Development Mode: Live Customization
Starting with Frontend Dev Server
When you need to customize the UI, use the --with-frontend flag:
m start --with-frontend
This command:
- Auto-detects if
frontend/directory exists - Auto-scaffolds the template if missing (see
m init:frontendbelow) - Starts two servers concurrently:
- Backend (uvicorn) on port 8000 - API server
- Frontend (Vite) on port 5173 - Dev server with HMR
Development Workflow
# Terminal 1: Start both servers
m start --with-frontend
# Backend runs on http://127.0.0.1:8000
# Frontend runs on http://localhost:5173 (Vite shows the port)
# Edit files in frontend/src/
# Changes appear instantly with Hot Module Replacement (HMR)
# Press Ctrl+C to stop both servers
Auto-Reload Backend
Enable backend auto-reload for full-stack development:
m start --with-frontend --reload
Now both backend and frontend reload on file changes!
Creating Custom Frontends
Initialize Frontend Template
Create a new frontend from the bundled template:
# Initialize in ./frontend
m init:frontend
# Or specify custom location
m init:frontend ../my-custom-ui
This scaffolds a complete React + TypeScript + Vite application with:
- @framework-m/desk npm package (providers pre-configured)
- Refine.dev setup for rapid admin UI development
- React Router for client-side routing
- Tailwind CSS + shadcn/ui components
- Vite for fast development and optimized builds
Template Structure
frontend/
├── package.json # Dependencies and scripts
├── .npmrc # GitLab registry configuration
├── vite.config.ts # Vite build configuration
├── tsconfig.json # TypeScript configuration
├── src/
│ ├── index.tsx # Application entry point
│ ├── App.tsx # Refine.dev setup
│ ├── pages/ # Page components
│ │ ├── Dashboard.tsx
│ │ └── Login.tsx
│ └── components/ # Reusable components
├── public/ # Static assets
└── README.md # Setup instructions
Install Dependencies
After scaffolding, install dependencies:
cd frontend
pnpm install
Run Development Server
# Option 1: Run frontend only
cd frontend
pnpm dev
# Option 2: Run both backend + frontend (recommended)
cd ..
m start --with-frontend
Using @framework-m/desk Package
What is @framework-m/desk?
The @framework-m/desk npm package provides pre-built providers for Refine.dev:
frameworkMDataProvider- CRUD operations via REST APIauthProvider- Cookie-based authenticationliveProvider- WebSocket real-time updates- Constants - API URLs configured from environment
This package is automatically included in templates created with m init:frontend.
Basic Usage
import { Refine } from "@refinedev/core";
import { BrowserRouter } from "react-router-dom";
import {
frameworkMDataProvider,
authProvider,
liveProvider,
} from "@framework-m/desk";
export default function App() {
return (
<BrowserRouter>
<Refine
dataProvider={frameworkMDataProvider}
authProvider={authProvider}
liveProvider={liveProvider}
resources={[
{
name: "User",
list: "/users",
show: "/users/:id",
edit: "/users/:id/edit",
create: "/users/new",
},
]}
>
{/* Your application routes */}
</Refine>
</BrowserRouter>
);
}
API Configuration
The package uses globalThis.__FRAMEWORK_CONFIG__ for API URLs:
// Set in index.html or environment
declare global {
interface Window {
__FRAMEWORK_CONFIG__?: {
API_URL?: string;
META_URL?: string;
WS_URL?: string;
};
}
}
// Default values (from vite.config.ts proxy)
globalThis.__FRAMEWORK_CONFIG__ = {
API_URL: "http://127.0.0.1:8000/api/v1",
META_URL: "http://127.0.0.1:8000/api/v1/meta",
WS_URL: "ws://127.0.0.1:8000/api/v1/live",
};
Authentication Flow
The authProvider handles cookie-based authentication:
// Login
await authProvider.login({
email: "admin@example.com",
password: "password",
});
// Check authentication status
const { authenticated } = await authProvider.check();
// Get current user identity
const identity = await authProvider.getIdentity();
// Logout
await authProvider.logout();
Real-time Updates
The liveProvider enables WebSocket subscriptions:
import { liveProvider, closeAllConnections } from "@framework-m/desk";
// Subscribe to User changes
liveProvider.subscribe({
channel: "User",
types: ["created", "updated", "deleted"],
callback: event => {
console.log("User changed:", event);
},
});
// Cleanup on unmount
useEffect(() => {
return () => closeAllConnections();
}, []);
GitLab Registry Configuration
Why GitLab Registry?
The @framework-m/desk package is published to your project's GitLab Package Registry (private by default). This ensures:
- Version control - Package versions tied to git tags
- Private packages - Not exposed to public npm registry
- CI/CD integration - Automatic publishing on releases
Configure Access
The template includes a .npmrc file:
@framework-m:registry=https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/packages/npm/
Local Development
For local development outside CI/CD, create a Personal Access Token:
- GitLab Settings → Access Tokens
- Create token with
read_apiscope - Configure npm:
# In your frontend directory
echo "//gitlab.com/api/v4/projects/${CI_PROJECT_ID}/packages/npm/:_authToken=${YOUR_TOKEN}" >> .npmrc
CI/CD Authentication
In GitLab CI/CD, authentication is automatic:
# .gitlab-ci.yml
install-frontend:
script:
- cd frontend
- echo "@framework-m:registry=https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/packages/npm/" > .npmrc
- echo "//gitlab.com/api/v4/projects/${CI_PROJECT_ID}/packages/npm/:_authToken=${CI_JOB_TOKEN}" >> .npmrc
- pnpm install
The CI_JOB_TOKEN is automatically available in GitLab CI/CD pipelines.
Publishing Updates
To publish a new version of @framework-m/desk:
# Update version in package.json
cd libs/framework-m-desk
npm version patch # or minor, major
# Create git tag
git tag framework-m-desk@v0.1.1
git push origin framework-m-desk@v0.1.1
# GitLab CI will build and publish automatically
CLI Reference
m start
Start Framework M application with bundled Desk UI.
m start [OPTIONS]
Options:
--port PORT- Backend port (default: 8000)--host HOST- Host to bind to (default: 127.0.0.1)--reload- Enable backend auto-reload--with-frontend- Start Vite dev server for frontend customization--frontend-dir PATH- Frontend directory path (default: ./frontend)--app APP- App path in module:attribute format
Examples:
# Production mode - bundled UI
m start
# Development mode - backend + Vite
m start --with-frontend
# Auto-reload backend
m start --reload
# Custom port
m start --port 3000
# Full-stack development with auto-reload
m start --with-frontend --reload --port 3000
m init:frontend
Initialize a new frontend from template.
m init:frontend [TARGET]
Arguments:
TARGET- Target directory for frontend (default: ./frontend)
Examples:
# Initialize in ./frontend
m init:frontend
# Initialize in custom directory
m init:frontend ../my-ui
# Initialize in specific path
m init:frontend /path/to/custom-frontend
What It Does:
- Copies template from
apps/studio/frontend - Creates directory structure with all configuration files
- Runs
pnpm installto install dependencies - Provides instructions for next steps
Deployment
Building for Production
cd frontend
pnpm build
This creates optimized static files in frontend/dist/.
Serving Production Build
The production build can be served in two ways:
Option 1: Bundle with PyPI package (recommended)
Include the built frontend in your PyPI distribution:
# Build frontend
cd frontend
pnpm build
# Copy to package static directory
cp -r dist/* ../libs/framework-m/src/framework_m/static/
# Build Python package
cd ../libs/framework-m
uv build
# Deploy
pip install dist/framework_m-*.whl
m start
Option 2: Separate static file server
Serve frontend/dist/ with nginx, Caddy, or any static file server. Configure CORS to allow API requests from your frontend domain.
Environment Variables
Configure API endpoints for production:
<!-- frontend/index.html -->
<script>
window.__FRAMEWORK_CONFIG__ = {
API_URL: "https://api.yourdomain.com/api/v1",
META_URL: "https://api.yourdomain.com/api/v1/meta",
WS_URL: "wss://api.yourdomain.com/api/v1/live",
};
</script>
Or use environment variables with Vite:
# .env.production
VITE_API_URL=https://api.yourdomain.com/api/v1
VITE_META_URL=https://api.yourdomain.com/api/v1/meta
VITE_WS_URL=wss://api.yourdomain.com/api/v1/live
Troubleshooting
Frontend Not Auto-Scaffolding
Symptom: m start --with-frontend doesn't create frontend directory
Solution: Run m init:frontend manually first:
m init:frontend
m start --with-frontend
npm Package Not Found
Symptom: npm ERR! 404 Not Found - GET https://gitlab.com/...
Solutions:
- Check
.npmrcconfiguration:cat frontend/.npmrc - Verify CI_PROJECT_ID is set correctly
- Create Personal Access Token for local development
- Check package registry permissions in GitLab
Port Already in Use
Symptom: Error: Address already in use
Solutions:
# Use different port
m start --port 8001
# Kill process using port 8000
lsof -ti:8000 | xargs kill -9
WebSocket Connection Failed
Symptom: Live updates not working
Solutions:
- Check WS_URL in configuration
- Verify WebSocket endpoint is accessible
- Check firewall/proxy settings allow WebSocket connections
- Use secure WebSocket (wss://) in production with HTTPS
Build Failures
Symptom: pnpm build fails with errors
Solutions:
- Clear node_modules and reinstall:
rm -rf node_modules pnpm-lock.yaml
pnpm install - Check Node.js version (requires Node 20+)
- Verify TypeScript errors:
pnpm tsc --noEmit
Best Practices
Development Workflow
- Start with bundled UI - Verify backend works first
- Scaffold frontend - Only when customization is needed
- Use
--with-frontend- Simplifies dual-server management - Enable
--reload- For full-stack development - Commit often - Frontend changes tracked in git
Project Structure
my-project/
├── app.py # Backend application entry
├── frontend/ # Custom frontend (gitignored by default)
│ ├── src/
│ │ ├── App.tsx
│ │ └── pages/
│ └── package.json
├── models/ # Your DocTypes
├── controllers/ # Business logic
└── pyproject.toml # Python dependencies
Version Control
Recommended .gitignore:
# Frontend
frontend/node_modules/
frontend/dist/
frontend/.npmrc # Contains auth tokens
# Python
__pycache__/
*.pyc
.venv/
dist/
*.egg-info/
Commit frontend source:
- ✅ Commit
frontend/src/,frontend/package.json,frontend/vite.config.ts - ❌ Don't commit
node_modules/,dist/,.npmrcwith tokens
Performance Optimization
- Use production builds -
pnpm buildcreates optimized bundles - Enable gzip/brotli - Compress static assets
- Use CDN - Serve static files from CDN in production
- Lazy load routes - Split code by route for faster initial load
- Cache API responses - Use Refine's built-in query caching
Next Steps
- DocType Guide - Learn how to create data models
- Protocol Reference - Explore protocol interfaces
- Refine Documentation - Learn Refine.dev framework
- Vite Documentation - Understand Vite build tool
Support
- GitHub Issues: Report bugs or request features
- Community Forum: Ask questions and share knowledge
- Documentation: Browse full documentation