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
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:
| Extra | Why 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 som prodcan 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_URLto 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 prodrefuses to start withoutFRAMEWORK_M_JWT_SECRET.m devrelaxes 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 createinstead.
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 prodlistens on port 8000 by default. Use--portto change it. (m devuses 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 iffrontend/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 buildcan fail withTS2882because the generated TypeScript config lacks Vite's CSS side-effect typings. Until the template is fixed, usepnpm exec vite build(as shown above), or addsrc/vite-env.d.tscontaining/// <reference types="vite/client" />and set"noEmit": trueintsconfig.json, then runpnpm 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 prodbinds0.0.0.0and port8000by 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/distfrom nginx or a CDN instead of the backend.
Troubleshooting
| Symptom | Fix |
|---|---|
No module named 'sqlalchemy' | Install [db]: uv add "framework-m-standard[db]" |
Alembic not initialized | Run uv run m migrate init before migrate sync / migrate all |
Missing required secrets: FRAMEWORK_M_JWT_SECRET | Add it to crm/.env |
Payload encryption requires the 'cryptography' package | Install [server] (or an engine extra, which includes it) |
No module named uvicorn / No module named granian | Install [server-uvicorn] or [server-granian] |
m: command not found | Run commands with uv run m ... from the app directory |
| Desk not reachable on port 8888 | m prod uses 8000; m dev uses 8888 |
| Sign-up email never arrives | Use m user create locally; self-signup needs SMTP + a job worker |