Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
1be532c
docs: app.state organization design
antosubash Apr 17, 2026
e94e292
docs: app.state organization implementation plan
antosubash Apr 17, 2026
ee3f8c0
feat(core): add Services dataclass for framework singletons
antosubash Apr 17, 2026
13ecb70
test(core): match codebase test-class convention for Services
antosubash Apr 17, 2026
697a163
feat(hosting): populate app.state.sm alongside loose keys
antosubash Apr 17, 2026
55c1b77
refactor(hosting): read framework singletons via app.state.sm
antosubash Apr 17, 2026
986acfe
refactor(hosting): read i18n + permission singletons via app.state.sm
antosubash Apr 17, 2026
d004be6
refactor(host): read i18n state via app.state.sm with fallback
antosubash Apr 17, 2026
7393dd4
refactor(modules): read framework state via app.state.sm
antosubash Apr 17, 2026
abd8d79
feat(users): introduce UsersServices at app.state.users
antosubash Apr 17, 2026
e580ebf
refactor(users): read module state via app.state.users
antosubash Apr 17, 2026
6bfe5e9
refactor(users): migrate bootstrap + its test helper to app.state.use…
antosubash Apr 17, 2026
9884ca6
test(users+auth): migrate remaining app.state reads to sm/users slots
antosubash Apr 17, 2026
3de9d99
refactor: delete deprecated loose app.state keys
antosubash Apr 17, 2026
c31ef12
refactor: migrate consumers to read from app.state.sm.*
antosubash Apr 17, 2026
c075ac6
fix: migrate remaining app.state reads missed in Task 10
antosubash Apr 17, 2026
329a94e
revert: restore generated i18n files (regen had stale fixture)
antosubash Apr 17, 2026
4f150c6
refactor: drop duplicate locale fields, update SM012, simplify i18n r…
antosubash Apr 17, 2026
e3d9dab
refactor(hosting): plumb inertia_config via return value instead of a…
antosubash Apr 17, 2026
c190e01
docs+scaffold: adopt app.state.sm + app.state.<module> convention
antosubash Apr 17, 2026
7cf9c78
fix(verify): migrate remaining app.state reads in test fixtures + lin…
antosubash Apr 17, 2026
373acca
cleanup: simplify defensive guards + remove stale migration comments
antosubash Apr 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ async def app(settings: Settings):

application = create_app(settings)

await _create_all_tables(application.state.db.engine)
await _create_all_tables(application.state.sm.db.engine)

# Trigger lifespan startup so app.state.migration is populated
ctx = application.router.lifespan_context(application)
Expand All @@ -146,7 +146,7 @@ async def client(app) -> AsyncGenerator[httpx.AsyncClient, None]:
requests from anonymous test flows (login, accept-invite, register) pass
validation without first making a GET to mint a token."""
signed = forge_session_cookie(
app.state.settings.secret_key, {SESSION_CSRF_TOKEN_KEY: _TEST_CSRF_TOKEN}
app.state.sm.settings.secret_key, {SESSION_CSRF_TOKEN_KEY: _TEST_CSRF_TOKEN}
)
transport = httpx.ASGITransport(app=app)
async with httpx.AsyncClient(
Expand All @@ -163,7 +163,7 @@ async def authenticated_client(app) -> AsyncGenerator[httpx.AsyncClient, None]:
"""HTTPX client with a signed session cookie carrying a seeded admin user's id."""
from users.bootstrap import create_admin

async with app.state.db.session_factory() as session:
async with app.state.sm.db.session_factory() as session:
result = await create_admin(
session,
email="admin@test",
Expand All @@ -173,7 +173,7 @@ async def authenticated_client(app) -> AsyncGenerator[httpx.AsyncClient, None]:
user_id = str(result.user.id)

signed = forge_session_cookie(
app.state.settings.secret_key,
app.state.sm.settings.secret_key,
{"user_id": user_id, SESSION_CSRF_TOKEN_KEY: _TEST_CSRF_TOKEN},
)

Expand Down
27 changes: 23 additions & 4 deletions docs/framework-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,15 +92,34 @@ SM_DATABASE_URL, SM_ENVIRONMENT, SM_SECRET_KEY, SM_VITE_DEV_URL,
SM_DEBUG, SM_LOG_LEVEL, SM_LOG_FORMAT, SM_MULTI_TENANT, SM_TENANT_HEADER
```

### Framework state

Framework singletons live on `app.state.sm`, a frozen `Services` dataclass populated
once at boot. Consumers read `request.app.state.sm.<field>` — never raw `app.state`
attributes for framework-owned state.

Fields: `settings`, `db`, `event_bus`, `menu_registry`, `permissions`,
`feature_flags`, `health_registry`, `i18n_registry`, `inertia_config`, `modules`.

Two attributes are intentionally kept outside `Services`:

- `app.state.inertia_dependency` — request-scoped `Depends` factory from fastapi-inertia.
- `app.state.migration` — dev-only boot-time check result, set in lifespan.

### Module settings

Module settings should:
- Use a per-module prefix: `SM_<MODULE>_*` (e.g. `SM_USERS_ALLOW_SIGNUP`).
- Be stored on `app.state.<module>_settings` during `register_settings(app)`.
- `SM012` diagnostic fires if `register_settings` is overridden but no `app.state.<module>_settings` is added.
- Be stored inside a module-owned dataclass at `app.state.<module_lower>` during `register_settings(app)`.
- `SM012` diagnostic fires if `register_settings` is overridden but no `app.state.<module_lower>` entry is added.

```python
class AuthModule(ModuleBase):
class UsersModule(ModuleBase):
def register_settings(self, app: FastAPI) -> None:
app.state.auth_settings = AuthSettings()
from users.services import UsersServices
from users.settings import UsersSettings

app.state.users = UsersServices(settings=UsersSettings())
```

## Database
Expand Down
Loading