Skip to main content

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:

  1. Uses honcho.manager.Manager for process orchestration
  2. Backend serves everything - UI assets + APIs
  3. Frontend (Vite) runs separately, proxies API to backend:8000
  4. Processes are hardcoded in Python dict
  5. 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 dev for 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:

  1. Support Procfile loading
  2. Support m.toml [dev] section
  3. Set M_DEV_MODE env var
  4. Pass VITE_BACKEND_URL to frontend
  5. 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_static parameter to create_app()
  • Conditionally include UI routes based on serve_static
  • Check M_DEV_MODE environment variable
  • Update m prod to pass serve_static=True (production)
  • Test backend without static serving

Phase 2: Vite Proxy Configuration ✅ COMPLETE

  • Update frontend/vite.config.ts to use VITE_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=true in environment
  • Pass VITE_BACKEND_URL to 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

  1. libs/framework-m-standard/src/framework_m_standard/adapters/web/app.py
    • Add serve_static parameter to create_app()
    • Conditionally include UI routes
  2. apps/studio/src/framework_m_studio/cli/dev.py
    • Add Procfile support
    • Add m.toml [dev] config support
    • Set M_DEV_MODE and VITE_BACKEND_URL
    • Add --enable-worker, --enable-scheduler flags
  3. frontend/vite.config.ts
    • Use process.env.VITE_BACKEND_URL
  4. libs/framework-m/src/framework_m/templates/frontend/vite.config.ts
    • Use process.env.VITE_BACKEND_URL

New Files

  1. libs/framework-m-core/src/framework_m_core/dev_config.py (optional)
    • Schema for [dev] section validation
    • Procfile parser utilities

Documentation

  1. docs/developer/desk-setup.md

    • Updated m dev documentation
    • Procfile examples
    • m.toml [dev] examples
  2. docs/examples/Procfile.dev (new)

    • Example Procfile with all processes
  3. 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

  1. ✅ Separation of Concerns
    • Backend = API only in dev
    • Frontend = Vite dev server with HMR
  2. ✅ Extensibility
    • Add workers, schedulers easily
    • Custom processes via Procfile
  3. ✅ Configuration
    • m.toml for project defaults
    • Procfile for custom setups
    • CLI flags for quick overrides
  4. ✅ Developer Experience
    • No static file conflicts
    • Better HMR performance
    • Clear process separation
  5. ✅ 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

  1. Development and production workflows are clearly separated (m dev vs m prod).
  2. Backend static serving is disabled in dev mode, reducing conflicts with Vite HMR.
  3. Process orchestration is extensible through Procfile and m.toml [dev] configuration.
  4. Existing projects can adopt incrementally without breaking production startup behavior.