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
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:

  1. Auto-detects if frontend/ directory exists
  2. Auto-scaffolds the template if missing (see m init:frontend below)
  3. 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 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 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:

  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 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:

  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 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:

  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 start --port 8001

# Kill process using port 8000
lsof -ti:8000 | 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 --with-frontend - Simplifies dual-server management
  4. Enable --reload - For full-stack development
  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