Skip to main content

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:

  1. Use the bundled UI - Zero npm setup required
  2. Customize the bundled UI - Run Vite dev server for live development
  3. 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 in production mode
m prod

That's it! Open http://127.0.0.1:8000 in your browser. The Desk UI is pre-built and served automatically from the inner package assets.

How It Works

  • Pre-built static files are bundled in the PyPI framework-m-standard package
  • Served automatically by the backend when no custom frontend is active
  • No build step needed - zero-configuration setup for immediate use

Development Mode: Live Customization

When you need to customize the UI or build your own plugins, use m dev. This command is orchestration-heavy and requires Framework M Studio to be installed in your environment.

Starting with Frontend Dev Server

m dev

By default, m dev:

  1. Auto-detects if a frontend/ directory exists.
  2. Starts the Backend (uvicorn) with auto-reload.
  3. Starts the Frontend (Vite) concurrently with HMR (Hot Module Replacement).
  4. Starts Studio on port 9999 for visual editing.

Development Workflow

# Start the full dev stack
m dev

# Backend typically runs on http://127.0.0.1:8000
# Frontend runs on http://localhost:5173
# Studio runs on http://localhost:9999

# Edit files in frontend/src/ or use Studio to scaffold
# Changes appear instantly!

Creating Custom Frontends

Initialize Frontend Template (Requires Studio)

Scaffolding new frontends is a developer-only feature provided by the m studio toolkit. You cannot scaffold in a production environment as the templates and logic are not installed there.

# Initialize in ./frontend using studio CLI
m studio new frontend

# Or specify custom location
m studio new 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 prod --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 API
  • authProvider - Cookie-based authentication
  • liveProvider - WebSocket real-time updates
  • Constants - API URLs configured from environment

This package is automatically included in templates created with m new 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:8888/api/v1",
META_URL: "http://127.0.0.1:8888/api/v1/meta",
WS_URL: "ws://127.0.0.1:8888/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:

  1. GitLab SettingsAccess Tokens
  2. Create token with read_api scope
  3. 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 prod

Start Framework M in production mode.

m prod [OPTIONS]

Options:

  • --port PORT - Backend port (default: 8000)
  • --host HOST - Host to bind to (default: 0.0.0.0)
  • --with-frontend - Serve custom built assets from frontend/dist via backend.
  • --frontend-dir PATH - Frontend directory path (default: ./frontend)

Examples:

# Production mode - serve bundled assets
m prod

# Production mode - serve custom built assets from frontend/dist
m prod --with-frontend

# Custom port
m prod --port 3000

m dev (Development Only)

Standard full-stack development command. Starts backend, frontend (Vite), and Studio.

m dev [OPTIONS]

Options:

  • --port PORT - Backend port (default: 8000)
  • --studio-port PORT - Studio port (default: 9999)
  • --no-frontend - Disable the Vite frontend process.
  • --enable-worker - Start the Taskiq background worker concurrently.

m studio new frontend (Requires Studio)

Initialize a new frontend from the standard template.

m studio new frontend [TARGET]

Arguments:

  • TARGET - Target directory for frontend (default: ./frontend)

Examples:

# Initialize in ./frontend
m studio new frontend

# Initialize in custom directory
m studio new frontend ../my-ui

What It Does:

  1. Copies template from apps/studio/frontend
  2. Creates directory structure with all configuration files
  3. Runs pnpm install to install dependencies
  4. 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 prod

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.

Distributed UI & Macroservices

For large applications, you can decompose your UI into independent macroservices. See the Distributed UI Guide for more information on how to configure remote MFE discovery and transparent proxying.

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

Studio Features Not Found

Symptom: m studio new frontend or m dev fails with "Command not found".

Solution: Ensure framework-m-studio is installed in your development environment. This package is typically not included in production-lean images.

uv pip install framework-m-studio

npm Package Not Found

Symptom: npm ERR! 404 Not Found - GET https://gitlab.com/...

Solutions:

  1. Check .npmrc configuration:
    cat frontend/.npmrc
  2. Verify CI_PROJECT_ID is set correctly
  3. Create Personal Access Token for local development
  4. Check package registry permissions in GitLab

Port Already in Use

Symptom: Error: Address already in use

Solutions:

# Use different port
m prod --port 8001

# Kill process using port 8888
lsof -ti:8888 | xargs kill -9

WebSocket Connection Failed

Symptom: Live updates not working

Solutions:

  1. Check WS_URL in configuration
  2. Verify WebSocket endpoint is accessible
  3. Check firewall/proxy settings allow WebSocket connections
  4. Use secure WebSocket (wss://) in production with HTTPS

Build Failures

Symptom: pnpm build fails with errors

Solutions:

  1. Clear node_modules and reinstall:
    rm -rf node_modules pnpm-lock.yaml
    pnpm install
  2. Check Node.js version (requires Node 20+)
  3. Verify TypeScript errors:
    pnpm tsc --noEmit

Best Practices

Development Workflow

  1. Start with bundled UI - Verify backend works first
  2. Scaffold frontend - Only when customization is needed
  3. Use m prod --with-frontend - Simplifies dual-server management
  4. Hot reload is automatic - Backend and frontend reload on changes
  5. 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/, .npmrc with tokens

Performance Optimization

  1. Use production builds - pnpm build creates optimized bundles
  2. Enable gzip/brotli - Compress static assets
  3. Use CDN - Serve static files from CDN in production
  4. Lazy load routes - Split code by route for faster initial load
  5. Cache API responses - Use Refine's built-in query caching

Next Steps


Support