Project structure
smpy new myapp lays down a uv + npm workspace by default. The workspace root holds shared config; the runnable app lives under host/; your own modules live as workspace members under modules/ (seeded with a sample hello module).
myapp/
├── pyproject.toml # uv workspace root (members: host, modules/*) — not itself installed
├── package.json # npm workspace root (host/client_app, modules/*)
├── .env.example # default env vars; copy to .env
├── Makefile # thin wrapper: install / dev / migrate / build / gen-pages
├── README.md
│
├── host/ # the runnable application
│ ├── main.py # FastAPI entry point — create_app + lifespan
│ ├── routes.py
│ ├── pyproject.toml # host deps (uv-managed): hosting + selected modules
│ ├── alembic.ini # Alembic config — points at migrations/
│ ├── Makefile
│ ├── client_app/ # Vite + React root
│ │ ├── main.tsx # bootstrap
│ │ ├── app.tsx, pages.ts
│ │ ├── pages/ # host-level Inertia pages (Landing, Error)
│ │ ├── 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 workspace modules (smpy create-module --dest modules/<name>)
└── hello/ # sample module scaffolded by smpy new
└── … (see "Anatomy of a module" below)smpy new --flat skips the workspace wrapper and the sample hello module, emitting a single-host layout (main.py, client_app/, migrations/ at the top level) — use it when the app only consumes published modules and never authors its own. smpy new --preset minimal ships fewer pre-installed modules. A docker-compose.yml is added only when the background_tasks module is included.
Bundled modules
The framework's first-party modules live on PyPI as simple_module_<name> and are added to pyproject.toml by smpy 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 |
|---|---|---|
host/client_app/modules.generated.ts | make gen-pages (auto via make dev) | Every time you add/remove a module page |
host/client_app/modules.manifest.json | make gen-pages | Same |
host/client_app/modules.generated.css | make gen-pages | Same |
host/migrations/versions/XXXX_*.py | make migration msg="…" | When you add/change SQLModel tables |
modules/<name>/** | smpy 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 inhost/migrations/versions/. Autogenerate discovers every installed module's metadata viabuild_module_metadata()inhost/migrations/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. |