Skip to content

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.

text
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.py

sm 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:

ModuleWhat it provides
authTiny public API (UserContext, get_current_user, require_permission)
usersEmail + password login, sessions, roles, invites, admin UI
permissionsRole + direct-grant assignment store + RequiresPermission
settingsDB-backed key/value store + per-module pydantic settings + admin UI
feature_flagsRuntime feature toggles with system + tenant overrides
file_storagePluggable file storage (filesystem, S3) + browse/download UI
background_tasksCelery + Redis workers, persistent task history, retry, worker dashboard
dashboardAuthenticated 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:

text
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 needed

See the module authoring guide for the full contract.

Where things get generated

FileGenerated byWhen
client_app/modules.generated.tsmake gen-pages (auto via make dev)Every time you add/remove a module page
client_app/modules.manifest.jsonmake gen-pagesSame
client_app/modules.generated.cssmake gen-pagesSame
migrations/versions/XXXX_*.pyuv 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 in migrations/versions/. Autogenerate discovers every installed module's metadata via build_module_metadata() in migrations/env.py. Each module's first migration sets a branch_labels marker to enable alembic downgrade <module>@base.
  • No host-level api/ folder. REST endpoints are attached by each module via register_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's contracts/ so other modules import them by name — the reverse of a monolith's "shared schemas" directory.

Tools config that affects everyone

FileControls
pyproject.tomlRuff rules, ty config, pytest markers (asyncio_mode=auto, -m 'not e2e'), SQLModel-related ty-false-positive suppressions
client_app/biome.jsonJS/TS linting + formatting
client_app/vitest.config.tsJS unit-test setup
MakefileInner dev loop. If it's not make <target>, run the underlying uv / npm / alembic command directly.

Released under the MIT License.