Project structure
sm new myapp lays down a focused tree. The host owns the runnable app; modules live as packages under modules/; everything else is configuration.
myapp/
├── main.py # FastAPI entry point — create_app + lifespan
├── pyproject.toml # your app's deps (uv-managed)
├── alembic.ini # Alembic config — points at migrations/
├── .env.example # default env vars; copy to .env
├── docker-compose.yml # Postgres + Redis for local dev
├── Makefile # thin wrapper: dev / install / migrate / build / gen-pages
│
├── client_app/ # Vite + React root
│ ├── main.tsx # bootstrap
│ ├── pages/ # host-level Inertia pages (Landing, NotFound)
│ ├── modules.generated.ts # auto-generated page map — DO NOT EDIT
│ ├── modules.manifest.json # auto-generated manifest — DO NOT EDIT
│ ├── modules.generated.css # auto-generated module CSS — DO NOT EDIT
│ ├── package.json
│ ├── tsconfig.json
│ └── vite.config.ts
│
├── migrations/ # Alembic migrations for ALL modules
│ ├── env.py # calls build_module_metadata() to union every module's MetaData
│ └── versions/
│
├── modules/ # your custom modules go here (sm create-module --dest modules/<name>)
│ └── orders/ # example — generated by sm create-module orders
│ └── … (see "Anatomy of a module" below)
│
└── tests/ # your app-level tests
└── test_smoke.pysm new --flat skips the modules/ directory entirely, for the case where the app only consumes published modules and never authors its own. sm new --preset minimal ships fewer pre-installed modules.
Bundled modules
The framework's first-party modules live on PyPI as simple_module_<name> and are added to pyproject.toml by sm new based on the preset / --with you pick:
| Module | What it provides |
|---|---|
auth | Tiny public API (UserContext, get_current_user, require_permission) |
users | Email + password login, sessions, roles, invites, admin UI |
permissions | Role + direct-grant assignment store + RequiresPermission |
settings | DB-backed key/value store + per-module pydantic settings + admin UI |
feature_flags | Runtime feature toggles with system + tenant overrides |
file_storage | Pluggable file storage (filesystem, S3) + browse/download UI |
background_tasks | Celery + Redis workers, persistent task history, retry, worker dashboard |
dashboard | Authenticated landing page |
See Bundled modules for details on each.
Anatomy of a module
Every module under modules/<name>/ (or installed from PyPI as simple_module_<name>) follows the same shape:
modules/orders/
├── pyproject.toml # entry point: simple_module = "orders.module:OrdersModule"
├── package.json # npm workspace member (for page TSX + Biome)
├── tsconfig.json # TypeScript project for the pages
├── orders/
│ ├── __init__.py
│ ├── module.py # ModuleBase subclass with meta = ModuleMeta(...)
│ ├── models.py # SQLModel tables (optional — some modules are UI-only)
│ ├── contracts/ # SQLModel DTOs — the PUBLIC surface for other modules
│ │ └── schemas.py
│ ├── service.py # business logic — takes AsyncSession, returns DTOs
│ ├── services.py # module-scoped state container — assigned to app.state.orders
│ ├── deps.py # FastAPI dependencies (auth requirements, etc.)
│ ├── endpoints/
│ │ ├── api.py # REST (JSON) endpoints
│ │ └── views.py # Inertia view endpoints
│ ├── pages/ # *.tsx — auto-discovered by Vite via modules.generated.ts
│ │ ├── Browse.tsx
│ │ ├── Create.tsx
│ │ └── Edit.tsx
│ └── locales/
│ └── en.json # translations, namespaced by module name
└── tests/
└── test_orders.py # smoke test — extend with service/endpoint tests as neededSee the module authoring guide for the full contract.
Where things get generated
| File | Generated by | When |
|---|---|---|
client_app/modules.generated.ts | make gen-pages (auto via make dev) | Every time you add/remove a module page |
client_app/modules.manifest.json | make gen-pages | Same |
client_app/modules.generated.css | make gen-pages | Same |
migrations/versions/XXXX_*.py | uv run alembic revision --autogenerate -m "…" | When you add/change SQLModel tables |
modules/<name>/** | sm create-module <name> --dest modules/<name> | Scaffolding a new module |
Never hand-edit the .generated.* files — they are overwritten on the next make gen-pages run.
Where things intentionally don't live
- No per-module
migrations/folder. All migrations live inmigrations/versions/. Autogenerate discovers every installed module's metadata viabuild_module_metadata()inmigrations/env.py. Each module's first migration sets abranch_labelsmarker to enablealembic downgrade <module>@base. - No host-level
api/folder. REST endpoints are attached by each module viaregister_routes(api_router, view_router)./api/*is the union of every module's API router. - No
schemas/top-level folder. DTOs live inside the owning module'scontracts/so other modules import them by name — the reverse of a monolith's "shared schemas" directory.
Tools config that affects everyone
| File | Controls |
|---|---|
pyproject.toml | Ruff rules, ty config, pytest markers (asyncio_mode=auto, -m 'not e2e'), SQLModel-related ty-false-positive suppressions |
client_app/biome.json | JS/TS linting + formatting |
client_app/vitest.config.ts | JS unit-test setup |
Makefile | Inner dev loop. If it's not make <target>, run the underlying uv / npm / alembic command directly. |