ADR-0009: m dev Command Redesign
- Status: Accepted
- Date: 2026-02-19
- Deciders: @anshpansuriya14
- Supersedes: N/A
- Superseded by: N/A
Context
Current Implementation
Located in: apps/studio/src/framework_m_studio/cli/dev.py
How it works now:
- Uses
honcho.manager.Managerfor process orchestration - Backend serves everything - UI assets + APIs
- Frontend (Vite) runs separately, proxies API to backend:8000
- Processes are hardcoded in Python dict
- Backend serves static files from bundled package
Current flow:
m dev
→ Backend (uvicorn) :8888 [serves static + API]
→ Frontend (Vite) :5173 [dev server, proxies /api → :8000]
→ Studio (optional) :9999
Issues:
- Backend port is 8888 by default, Vite proxies to 8000 (mismatch!)
- Backend serves static files (unnecessary in dev mode)
- Not extensible - can't add workers/schedulers easily
- Vite proxy config is hardcoded in vite.config.ts
Decision
1. Backend API-Only Mode (Development)
Goal: Backend should NOT serve UI assets in dev mode, only APIs
Implementation:
# In create_app()
def create_app(serve_static: bool = True) -> Litestar:
# ...
route_handlers: list[Any] = [
health_check,
auth_routes_router,
metadata_router,
workflow_router,
]
# Only add UI routes in production mode
if serve_static:
route_handlers.extend([
root_redirect, # / → /desk/
serve_desk_root, # /desk/ → HTML
serve_desk_spa, # /desk/* → HTML catchall
])
static_files_config = _create_static_files_config()
else:
static_files_config = []
app = Litestar(
route_handlers=route_handlers,
static_files_config=static_files_config,
# ...
)
Detection:
- Check environment variable:
M_DEV_MODE=true - Use
m devfor development workflows
2. Vite Proxy Configuration
Goal: Make backend URL configurable via environment
Current vite.config.ts:
proxy: {
"/api": {
target: "http://localhost:8000", // ❌ Hardcoded
changeOrigin: true,
},
}
Proposed vite.config.ts:
const BACKEND_URL = process.env.VITE_BACKEND_URL || "http://localhost:8888";
export default defineConfig({
server: {
port: 5173,
proxy: {
"/api": {
target: BACKEND_URL,
changeOrigin: true,
},
"/health": {
target: BACKEND_URL,
changeOrigin: true,
},
},
},
});
In dev.py:
env["VITE_BACKEND_URL"] = f"http://{host}:{port}"
3. Honcho Configuration File
Goal: Support Procfile for custom processes (workers, schedulers, etc.)
Create dev.toml or use m.toml section:
[dev]
# Default processes (can be overridden)
backend.command = "uvicorn app:create_app --host 127.0.0.1 --port 8888 --reload"
backend.env = { M_DEV_MODE = "true" }
frontend.command = "pnpm dev --port 5173"
frontend.cwd = "frontend"
frontend.env = { VITE_BACKEND_URL = "http://127.0.0.1:8888" }
# Optional processes
[[dev.process]]
name = "worker"
command = "m worker --concurrency 4"
enabled = false # User can enable
[[dev.process]]
name = "scheduler"
command = "m worker --scheduler-only"
enabled = false
[[dev.process]]
name = "custom"
command = "python scripts/watch_migrations.py"
enabled = false
Alternative: Support Procfile directly:
# Procfile.dev
backend: uvicorn app:create_app --host 127.0.0.1 --port 8888 --reload
frontend: cd frontend && pnpm dev --port 5173
worker: m worker --concurrency 4
scheduler: m worker --scheduler-only
Usage:
m dev # Use defaults
m dev --procfile Procfile.dev # Custom processes
m dev --enable-worker # Enable worker from config
4. Updated dev.py Implementation
Key changes:
- Support Procfile loading
- Support m.toml [dev] section
- Set M_DEV_MODE env var
- Pass VITE_BACKEND_URL to frontend
- Allow custom processes
Pseudocode:
def dev_command(
app: str | None = None,
host: str = "127.0.0.1",
port: int = DEFAULT_BACKEND_PORT,
frontend_dir: Path = Path("frontend"),
frontend_port: int = DEFAULT_FRONTEND_PORT,
no_frontend: bool = False,
enable_worker: bool = False,
enable_scheduler: bool = False,
procfile: Path | None = None,
studio: bool = False,
) -> None:
processes: dict[str, str] = {}
# Load from Procfile if provided
if procfile and procfile.exists():
processes = _load_procfile(procfile)
else:
# Load from m.toml [dev] section
config = load_config()
dev_config = config.get("dev", {})
if dev_config:
processes = _load_dev_config(dev_config)
else:
# Use defaults
processes = _get_default_processes(
app, host, port, frontend_dir, frontend_port, no_frontend
)
# Enable optional processes
if enable_worker:
processes["worker"] = "m worker --concurrency 4"
if enable_scheduler:
processes["scheduler"] = "m worker --scheduler-only"
if studio:
processes["studio"] = f"uvicorn {DEFAULT_STUDIO_APP} --port 9999 --reload"
# Set environment
env = os.environ.copy()
env["M_DEV_MODE"] = "true"
env["VITE_BACKEND_URL"] = f"http://{host}:{port}"
env["PYTHONPATH"] = _get_pythonpath()
# Run with honcho
manager = Manager(Printer(output=sys.stdout))
for name, cmd in processes.items():
manager.add_process(name, cmd, env=env)
# Signal handling
def signal_handler(signum: int, frame: object) -> None:
manager.terminate()
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
manager.loop()
Implementation Steps
Phase 1: Backend API-Only Mode ✅ COMPLETE
- Add
serve_staticparameter tocreate_app() - Conditionally include UI routes based on
serve_static - Check
M_DEV_MODEenvironment variable - Update
m prodto passserve_static=True(production) - Test backend without static serving
Phase 2: Vite Proxy Configuration ✅ COMPLETE
- Update
frontend/vite.config.tsto useVITE_BACKEND_URL - Update
libs/framework-m/src/framework_m/templates/frontend/vite.config.ts - Document environment variable usage
Phase 3: Configuration Support ✅ COMPLETE
- Design
m.toml[dev] schema - Implement
_load_dev_config()function - Add Procfile parser
_load_procfile() - Add CLI flags:
--enable-worker,--enable-scheduler,--procfile
Phase 4: Update dev.py ✅ COMPLETE
- Refactor
dev_command()with new logic - Set
M_DEV_MODE=truein environment - Pass
VITE_BACKEND_URLto frontend process - Support custom process definitions
Phase 5: Documentation & Testing ✅ COMPLETE
- Update
docs/developer/desk-setup.md - Add examples of custom Procfile
- Add examples of m.toml [dev] section
- Update tests in
apps/studio/tests/test_dev.py - Test with workers, schedulers
File Changes Summary
Modified Files
libs/framework-m-standard/src/framework_m_standard/adapters/web/app.py- Add
serve_staticparameter tocreate_app() - Conditionally include UI routes
- Add
apps/studio/src/framework_m_studio/cli/dev.py- Add Procfile support
- Add m.toml [dev] config support
- Set
M_DEV_MODEandVITE_BACKEND_URL - Add
--enable-worker,--enable-schedulerflags
frontend/vite.config.ts- Use
process.env.VITE_BACKEND_URL
- Use
libs/framework-m/src/framework_m/templates/frontend/vite.config.ts- Use
process.env.VITE_BACKEND_URL
- Use
New Files
libs/framework-m-core/src/framework_m_core/dev_config.py(optional)- Schema for [dev] section validation
- Procfile parser utilities
Documentation
-
docs/developer/desk-setup.md- Updated
m devdocumentation - Procfile examples
- m.toml [dev] examples
- Updated
-
docs/examples/Procfile.dev(new)- Example Procfile with all processes
-
docs/examples/m.toml(new)- Example m.toml with [dev] section
Configuration Examples
Example 1: Basic m.toml with dev section
[database]
url = "postgresql://localhost/myapp_dev"
[dev]
backend_port = 8888
frontend_port = 5173
# Enable optional processes
enable_worker = true
enable_scheduler = false
# Override backend command
backend_command = "uvicorn app:app --reload --port 8888"
Example 2: Advanced Procfile
# Procfile.dev
backend: M_DEV_MODE=true uvicorn app:create_app --host 0.0.0.0 --port 8888 --reload
frontend: cd frontend && VITE_BACKEND_URL=http://localhost:8888 pnpm dev
worker: m worker --concurrency 8
scheduler: m worker --scheduler-only --log-level debug
mailhog: mailhog -smtp-bind-addr 127.0.0.1:1025 -ui-bind-addr 127.0.0.1:8025
Example 3: Mixed approach
# m.toml - Only override what's needed
[dev]
backend_port = 9000 # Change default
enable_worker = true
# Use Procfile for the rest
procfile = "Procfile.dev"
Benefits
- ✅ Separation of Concerns
- Backend = API only in dev
- Frontend = Vite dev server with HMR
- ✅ Extensibility
- Add workers, schedulers easily
- Custom processes via Procfile
- ✅ Configuration
- m.toml for project defaults
- Procfile for custom setups
- CLI flags for quick overrides
- ✅ Developer Experience
- No static file conflicts
- Better HMR performance
- Clear process separation
- ✅ Production Parity
m prod= production (bundled static)m dev= development (separate servers)
Questions & Decisions
Q: Should we support both Procfile AND m.toml?
A: Yes - Procfile for complex setups, m.toml for simple config. Precedence: CLI flags > Procfile > m.toml > defaults
Q: How to handle port conflicts?
A: Auto-detect (try port, increment if taken) or fail fast with clear error
Q: Should backend auto-detect dev mode?
A: Yes - check M_DEV_MODE env var. Command sets it automatically.
Q: What about CORS in dev mode?
A: Already handled - CORS is permissive in backend. Vite proxy handles /api requests.
Q: Should we bundle Procfile in templates?
A: Yes - include commented Procfile.dev in m init:frontend output
Consequences
- Development and production workflows are clearly separated (
m devvsm prod). - Backend static serving is disabled in dev mode, reducing conflicts with Vite HMR.
- Process orchestration is extensible through Procfile and
m.toml[dev]configuration. - Existing projects can adopt incrementally without breaking production startup behavior.