Skip to main content

Getting Started

This guide takes you from an empty directory to a running Framework M app with the bundled Desk UI, a database, and a working login.

Quick Start​

Open in GitHub Codespaces Use devcontainer

Prefer a preconfigured environment? See Develop in a Devcontainer.


1. What you need​

  • Python 3.12+
  • uv — install
  • Node.js + pnpm — only needed for the optional custom frontend

2. Create the project​

Choose the setup that matches where you are working.

Choose a Setup​

Local setup​

The parent project is only a workspace for the scaffolding tools. The app you create next is its own uv project.

uv init my-project
cd my-project

# Framework runtime + the `m new ...` scaffolding commands
uv add framework-m
uv add --dev framework-m-studio

Devcontainer​

If you are using the Framework M devcontainer, the parent project is already configured. Open the container first, then skip the uv init/uv add steps above.

The container provides PostgreSQL, Redis, and NATS. Skip the SQLite .env in step 3; the container's DATABASE_URL is already set. For the app extras, use the Devcontainer Postgres command below.

The container forwards 8000 (m prod), 8888 (m dev API), 5173 (Vite), and 9999 (Studio). Use m dev for development or m prod for the production-style server.

Create a app​

Create the app and move into it:

uv run m new app crm
cd crm
uv sync

Install runtime extras​

Install the runtime extras inside the app (extras belong to the project that runs the app).

Local SQLite​

uv add "framework-m-standard[db,server-granian]" aiosqlite

What these provide:

ExtraWhy it is needed
[db]SQLAlchemy + Alembic for any SQL database. SQLite also needs the aiosqlite driver.
[server-granian]The [server] utilities (cryptography) plus Granian; recommended for production.
[server-uvicorn]Alternative to [server-granian], using Uvicorn as the ASGI engine.

[server] on its own does not include an ASGI engine. Use one of the two engine extras above so m prod can start the server.

Devcontainer Postgres​

The devcontainer uses PostgreSQL. Install the Postgres driver and engine extra, plus the sync driver used by migrations:

uv add "framework-m-standard[server-granian,postgres]" psycopg2-binary

[postgres] includes [db] and the runtime driver (asyncpg). psycopg2-binary is only needed for the synchronous reflection step used by migrate sync / migrate all.


3. Configure the app​

Devcontainer users: skip this step. The container already sets DATABASE_URL to its PostgreSQL service.

Create crm/.env (the same directory where you run uv run m ...):

DATABASE_URL=sqlite+aiosqlite:///./dev.db

# Required to start in production mode. For just development it is not needed.
# Use any random string of 32+ chars.
# Generate one with: openssl rand -hex 32
FRAMEWORK_M_JWT_SECRET=<random-generated-value>

You can generate a secret with:

openssl rand -hex 32

m prod refuses to start without FRAMEWORK_M_JWT_SECRET. m dev relaxes this, for a full demo we will set this for now.


4. Create a DocType​

uv run m new doctype Contact

The scaffold writes a working DocType, controller, and test under src/crm/doctypes/contact/. To give it some fields, edit src/crm/doctypes/contact/doctype.py:

from __future__ import annotations

from typing import ClassVar

from framework_m import DocType, Field


class Contact(DocType):
__doctype_name__: ClassVar[str] = "Contact"

first_name: str = Field(description="First name")
last_name: str = Field(description="Last name")
email: str = Field(default="", description="Email address")

class Meta:
requires_auth: ClassVar[bool] = False
apply_rls: ClassVar[bool] = False
naming_rule: ClassVar[str] = "autoincrement"
api_resource: ClassVar[bool] = True
show_in_desk: ClassVar[bool] = True
permissions: ClassVar[dict[str, list[str]]] = {
"create": ["All"],
"read": ["All"],
"write": ["All"],
"delete": ["All"],
}

The Meta defaults above keep the Contact usable without authentication while you are getting started.


5. Create the database schema​

Initialize Alembic once, then sync the schema from your DocTypes:

uv run m migrate init
uv run m migrate sync

migrate init only needs to be run once per project. After adding or changing DocTypes, run migrate sync again (and restart the server, because DocTypes are discovered at startup).


6. Create the first user​

By default, no user is automatically created. So create the first local user from the CLI:

uv run m user create \
--email admin@local.com \
--password 'ChangeMe123!' \
--name "Local Admin" \
--role Admin

Do not use the Desk's Sign Up flow for local development. It sends a verification email through the background job queue. With no SMTP server and no worker running, the email is never delivered and the account is never completed. Use m user create instead.


7. Run the app​

uv run m prod

Open:

http://127.0.0.1:8000/desk/

Sign in with the email and password from step 6.

m prod listens on port 8000 by default. Use --port to change it. (m dev uses port 8888 — see step 9.)


8. Check the API​

With the server running:

curl http://127.0.0.1:8000/health
curl http://127.0.0.1:8000/api/meta/doctypes

/health returns 200. /api/meta/doctypes returns the registered DocTypes, including Contact.


9. Development mode​

For day-to-day development, use:

uv run m dev

This starts:

  • backend on http://127.0.0.1:8888
  • Vite dev server on http://127.0.0.1:5173 (only if frontend/ exists, see step 10)

If there is no custom frontend, m dev runs the backend only and serves the bundled Desk.


10. Optional: custom frontend​

Create a frontend for your app:

uv run m new frontend

The scaffold creates ./frontend and installs its dependencies with pnpm. Build it:

cd frontend
pnpm exec vite build
cd ..

Then serve the prebuilt output from the backend:

uv run m prod --with-frontend

--with-frontend serves frontend/dist; it does not run the build for you.

Current scaffold limitation: pnpm build can fail with TS2882 because the generated TypeScript config lacks Vite's CSS side-effect typings. Until the template is fixed, use pnpm exec vite build (as shown above), or add src/vite-env.d.ts containing /// <reference types="vite/client" /> and set "noEmit": true in tsconfig.json, then run pnpm build.


11. Production notes​

  • Use uv add "framework-m-standard[server-granian,postgres]" for a PostgreSQL production setup.
  • Set a strong FRAMEWORK_M_JWT_SECRET (32+ random characters).
  • m prod binds 0.0.0.0 and port 8000 by default: uv run m prod --host 0.0.0.0 --port 8000.
  • Build the custom frontend before m prod --with-frontend.
  • For high-scale deployments, serve frontend/dist from nginx or a CDN instead of the backend.

Troubleshooting​

SymptomFix
No module named 'sqlalchemy'Install [db]: uv add "framework-m-standard[db]"
Alembic not initializedRun uv run m migrate init before migrate sync / migrate all
Missing required secrets: FRAMEWORK_M_JWT_SECRETAdd it to crm/.env
Payload encryption requires the 'cryptography' packageInstall [server] (or an engine extra, which includes it)
No module named uvicorn / No module named granianInstall [server-uvicorn] or [server-granian]
m: command not foundRun commands with uv run m ... from the app directory
Desk not reachable on port 8888m prod uses 8000; m dev uses 8888
Sign-up email never arrivesUse m user create locally; self-signup needs SMTP + a job worker

Next steps​