Skip to content

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).

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

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
host/client_app/modules.generated.tsmake gen-pages (auto via make dev)Every time you add/remove a module page
host/client_app/modules.manifest.jsonmake gen-pagesSame
host/client_app/modules.generated.cssmake gen-pagesSame
host/migrations/versions/XXXX_*.pymake 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 in host/migrations/versions/. Autogenerate discovers every installed module's metadata via build_module_metadata() in host/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.