Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

191 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

simple_module_python

A modular-monolith framework for Python. Each feature lives in its own self-contained module — its own SQLModel tables, FastAPI endpoints, React pages — but everything ships as one FastAPI + Inertia.js + React app. No microservice tax, no API-client glue; just plugin modules that compose at boot.

Stack

  • Backend: Python 3.12, FastAPI, SQLModel (SQLAlchemy async + Pydantic), Alembic
  • Frontend: Inertia.js + React + Tailwind CSS 4, Vite HMR
  • UI: shadcn/ui primitives + emerald/teal design tokens, Sora display font, DM Sans body, JetBrains Mono code
  • Auth: Pluggable providers — local users (email+password + OAuth/OIDC: Google, GitHub, Microsoft/Entra) via fastapi-users, or Keycloak OIDC SSO; cookie sessions or bearer tokens resolved through a principal-resolver chain
  • Tooling: uv workspaces, Ruff, ty, Biome, pytest

Use in a new project

If you want to build an app on simple_module, not hack on the framework itself:

uvx --from simple_module_cli smpy new my-app
cd my-app
make dev

That scaffolds a working FastAPI + Inertia + React app with users, dashboard, and permissions pre-wired. You land on /users/login, sign in with the admin account you bootstrap, and go from there.

See CHANGELOG.md for the list of published PyPI / npm packages at each release.


Quickstart

# 1. Install Python and JS deps
make install

# 2. Copy env template (defaults work for local SQLite dev)
cp .env.example .env

# 3. Start the shared dev-services stack — Postgres/Redis/MinIO (skip if using the default SQLite)
make docker-up

# 4. Run migrations
make migrate

# 5. Start API + Vite dev server in parallel
make dev

Hit http://localhost:8000 — you land on the public page. /users/login is the email+password login, /dashboard/ is the authenticated home, and /dashboard/doctor is the admin-only "smpy doctor" panel (static checks, migrations, dev server, modules).

Create a new module

make new-module name=orders

That scaffolds modules/orders/ with a working CRUD module end-to-end — ModuleMeta, SQLModel table with AuditMixin, SQLModel contracts, service layer, REST + Inertia view endpoints, Browse/Create/Edit.tsx pages, and tests. Next:

# 1. Edit modules/orders/orders/models.py to your actual schema
# 2. Generate a migration
make migration msg="add orders tables"
# 3. Apply it
make migrate
# 4. Run the scaffolded tests
make test

The new module is automatically discovered (via Python entry points), its routes register at /api/orders and /orders, and its sidebar entry appears in the menu.

Project layout

framework/
  cli/         # smpy CLI — scaffolding, skills, package updates
  core/        # module system, discovery, events, diagnostics
  db/          # per-module Base, session, mixins, listeners
  hosting/     # app_builder, middleware, settings, Inertia glue
  testing/     # shared pytest fixtures + helpers
modules/       # plugin modules (auth, dashboard, users, settings, ...)
host/
  main.py      # FastAPI entry point
  routes.py    # host-level routes (landing page)
  client_app/  # Vite + React client app
  migrations/  # Alembic migrations
packages/
  ui/          # shared shadcn/ui components, layouts, and design-system primitives
  i18n/        # generated i18n keys + translation runtime
scripts/
  new_module.py  # module scaffolder (called by `make new-module`)
docs/
  plans/                      # design + implementation plans
  framework-conventions.md    # invariants for module authors

Common commands

Command What it does
make install Install Python (uv sync) and JS (npm install) deps
make dev Docker up + API + Vite dev servers in parallel
make test Run the pytest suite
make lint Ruff + ty + Biome + tsc
make doctor Run module diagnostics (orphan pages, missing meta, coupling violations)
make migrate Apply pending Alembic migrations
make migration msg="..." Autogenerate a new migration
make new-module name=<name> Scaffold a new module
make kill Stop any running dev servers (ports 8000, 5050, 5173)
make docker-up / docker-down docker-up brings up the shared dev-services stack (Postgres/Redis/MinIO); docker-down stops only this repo's worker/beat (SQLite needs no Docker)

Configuration

Local deployments only need one env var — everything else has sensible defaults and is managed in the admin UI at /settings/modules.

Variable Default Required
SM_DATABASE_URL sqlite+aiosqlite:///./app.db Yes — async URL. Postgres: postgresql+asyncpg://...
SM_ENVIRONMENT development No — any value other than development, test, testing triggers strict discovery and placeholder-secret checks
SM_SECRET_KEY change-me-in-production No in dev; must be overridden in production
SM_VITE_DEV_URL http://localhost:5050 Dev only — Vite HMR origin
SM_AUTH_PROVIDER users No — users or keycloak. Only read when both are installed; see Auth providers

Power users can still override the following bootstrap knobs via env if needed: SM_DB_POOL_SIZE, SM_DB_MAX_OVERFLOW, SM_DB_POOL_PRE_PING, SM_DB_POOL_RECYCLE, SM_DEBUG, SM_LOG_LEVEL, SM_LOG_FORMAT, SM_MODULES_ENABLED. These are needed before the DB connection is open.

All module-level settings — users, SMTP, Celery broker, file storage backend, etc. — live in the admin UI. After upgrading an existing deployment, run once:

uv run smpy settings import-from-env

to seed DB overrides from the current SM_* environment.

docker-compose note: docker-compose.yml sets a few SM_BG_TASKS_* vars so Celery can reach the redis service by container hostname before the DB-backed settings are loaded. That's deployment plumbing — not a module config knob.

See framework-conventions.md for the settings-per-module convention.

UI & design system

The frontend uses an emerald + teal design system mirrored as Tailwind 4 tokens. Module pages should compose from a small set of shared primitives so they stay visually consistent without duplication.

Shared primitives (in packages/ui/src/):

Component When to use
PageShell Every authenticated page. Wraps title + description + actions header and a max-width content area.
StatCard Top-of-page KPI tiles — icon, value, label, optional delta badge. Used on Dashboard, Users, Doctor.
SectionTitle Card section headings with the gradient accent bar.
FilterPills Segmented filter chips for status/tab-style toggles.
AuthCardShell Login / register / forgot / accept-invite / verify — light glass card on emerald mesh blobs.
ErrorScreen 403 / 404 / 500 — gradient HTTP numerals + accent badge per status.

Design tokens live in packages/ui/src/styles/globals.css under the @theme block — primary emerald scale (--color-primary-50…900), display/sans/mono families, semantic shadcn tokens. Override the CSS variables to rebrand without touching component code.

Module pages should:

  • Wrap in PageShell with title, optional description, and actions.
  • Use Card + CardContent from @simple-module-py/ui/components/ui/card for content blocks.
  • Reach for StatCard / SectionTitle / FilterPills before rolling new layouts.
  • Use lucide-react icons (already a dependency) and the existing Badge / Button variants — emerald primary for the main CTA, outline / ghost for secondary actions.

The 300-line file cap (enforced by CI) usually pushes you to factor row-level components into pages/components/ — see modules/users/users/components/UserRow.tsx and modules/dashboard/dashboard/pages/components/doctor-data.ts for the pattern.

User management

Auth providers: users or keycloak, never both

This repo ships two authentication providers, and exactly one can be active at a time:

Module What it does
users (default) Local accounts — password login, invites, signup, roles, an admin UI
keycloak Delegates authentication to a Keycloak realm over OIDC

Both claim the same slot (app.state.auth.auth_provider), so running them together is a misconfiguration, not a supported combination. Installing both and activating neither is reported as SM020 (error — fails boot); installing neither is SM021.

uv sync --all-packages installs every workspace member, so a dev clone has both packages on disk. The host therefore activates one and skips the other rather than failing on SM020, and users is the defaultkeycloak is installed but inert until you ask for it. To switch:

# .env
SM_AUTH_PROVIDER=keycloak

Then re-run make gen-pages so the frontend manifest picks up the active provider's pages (make dev does this for you), and configure the realm under /settings/modules. Switching back is the same knob set to users.

Two caveats when running Keycloak:

  • dashboard, permissions, audit_log, and background_tasks declare a dependency on the Users module and import from the users package, so simple_module_users still has to be installed even though it is inactive. A host that wants Keycloak and none of the local-account machinery should leave those modules out of its own dependency list.
  • The local-account flows (/users/login, invites, signup, the sections below) belong to the users module and are gone while Keycloak is active — Keycloak's realm owns login, logout, and user administration instead.

Only one provider is ever discovered, so nothing here changes if you install just one of the two: a host that ships only keycloak keeps it regardless of SM_AUTH_PROVIDER.

Creating the first admin

Either use the CLI:

uv run smpy users create-admin --email admin@example.com --password changeme

Or let the app bootstrap it automatically on first boot by setting env vars before running make migrate && make dev:

SM_USERS_BOOTSTRAP_EMAIL=admin@example.com
SM_USERS_BOOTSTRAP_PASSWORD=changeme

The auto-bootstrap is idempotent — it only creates the user if the users_user table is empty.

Inviting users

  1. Log in as admin and navigate to /users/admin/invite.
  2. Fill in the invitee's email and optionally a full name and role(s). Click Send invite.
  3. With the default console mailer, the invite link is logged to stdout (tail -f the server log). Copy the link and send it to the user. With smtp, the email is delivered automatically.
  4. The invitee opens the link (/users/invite/accept?token=…), sets a password, and is immediately logged in.

Enabling public signup

Set SM_USERS_ALLOW_SIGNUP=true and restart the server. The /users/register page becomes accessible.

Switching to SMTP

SM_USERS_MAILER=smtp
SM_USERS_BASE_URL=https://your-domain.com
SM_USERS_SMTP_HOST=smtp.example.com
SM_USERS_SMTP_PORT=587
SM_USERS_SMTP_USERNAME=no-reply@example.com
SM_USERS_SMTP_PASSWORD=secret
SM_USERS_SMTP_FROM=no-reply@example.com
SM_USERS_SMTP_TLS=true

Architecture

  • Modules: discovered via Python entry points at boot. Each module subclasses ModuleBase and opts into the lifecycle hooks it needs (register_routes, register_menu_items, register_permissions, register_middleware, on_startup, ...).
  • Database: a single shared schema on both Postgres and SQLite. Each module owns its own MetaData (so Alembic can attribute tables to it), and __tablename__ is prefixed with the module name (orders_order) to avoid collisions.
  • Middleware pipeline (LIFO order of execution): CorrelationId → RequestLogging → SecurityHeaders → Session → <module middleware> → Tenant (opt-in) → Locale → InertiaLayoutData → app.
  • Diagnostics: make doctor runs a static analyzer over installed modules looking for orphan pages, phantom renders, empty modules, framework/plugin coupling, migration drift, and locale-file consistency. Errors fail the boot in production.
  • Internationalization: per-module locales/<lang>.json files merged at boot into I18nRegistry. Frontend uses i18next with type-safe keys; backend uses Babel for CLDR plurals. Locale resolved per request via cookie → Accept-LanguageSM_I18N_DEFAULT_LOCALE. See docs/framework-conventions.md → Internationalization.

Full documentation lives in docs/ — a VitePress site covering the guide, framework internals, database, frontend, testing, every bundled module, and reference. When conventions are ambiguous, the authoritative single-pagers are the source of truth:

Historical, point-in-time design docs live under docs/plans/ and docs/superpowers/.

Contributing

  • Write tests with the fixtures from the simple_module_test plugin (db_session, authenticated_client).
  • Lint with make lint before pushing; CI runs all four checks in parallel.
  • Stick to the conventions in docs/framework-conventions.md — they're what diagnostics enforce.

Releases

Used by

Contributors

Languages