diff --git a/CHANGELOG.md b/CHANGELOG.md
index e42f871..efc71c1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,37 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
---
+## v26.05.04 (2026-05-08)
+
+### Fixed — `pyfly.security` no longer needs `pyjwt` to import
+
+`pyfly.security/__init__.py` used to eagerly re-export
+`SecurityMiddleware`, which transitively imported the starlette
+adapter and `pyjwt` at module load time. The chain meant that
+`import pyfly` itself failed when those optional packages were
+missing — even for non-HTTP services that just want the kernel + DDD
+primitives.
+
+The import is now wrapped in `try / except ImportError`, matching the
+pattern already used for `JWTService` and `BcryptPasswordEncoder`.
+Optional symbols (`SecurityMiddleware`, `JWTService`,
+`BcryptPasswordEncoder`) only land in the `__all__` export list when
+their underlying packages (`starlette`, `pyjwt`, `bcrypt`) are present.
+
+Regression test pinned in
+[`tests/security/test_optional_imports.py`](tests/security/test_optional_imports.py).
+
+### Verified — bare-wheel install works end-to-end
+
+* `pip install pyfly` (no extras) → `pyfly.domain` primitives
+ importable, no infra deps required.
+* `pip install "pyfly[web,cqrs,transactional,eventsourcing]"` →
+ `PyFlyApplication`, `@enable_*_stack` decorators, and
+ `register_*_stack(app)` imperative API all work without `pyjwt`
+ installed.
+
+---
+
## v26.05.03 (2026-05-08)
### Changed — starter decorators are now functional
diff --git a/README.md b/README.md
index 6e983c1..86def17 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@
-
+
@@ -804,13 +804,13 @@ See **[`samples/order_service/`](samples/order_service/README.md)** for an end-t
```bash
# Install the latest release (uv)
-uv add "pyfly @ https://github.com/fireflyframework/fireflyframework-pyfly/releases/latest/download/pyfly-26.5.3-py3-none-any.whl"
+uv add "pyfly @ https://github.com/fireflyframework/fireflyframework-pyfly/releases/latest/download/pyfly-26.5.4-py3-none-any.whl"
# Install with specific extras
-uv add "pyfly[web,data-relational,cache] @ https://github.com/fireflyframework/fireflyframework-pyfly/releases/latest/download/pyfly-26.5.3-py3-none-any.whl"
+uv add "pyfly[web,data-relational,cache] @ https://github.com/fireflyframework/fireflyframework-pyfly/releases/latest/download/pyfly-26.5.4-py3-none-any.whl"
# Or with pip
-pip install "pyfly @ https://github.com/fireflyframework/fireflyframework-pyfly/releases/latest/download/pyfly-26.5.3-py3-none-any.whl"
+pip install "pyfly @ https://github.com/fireflyframework/fireflyframework-pyfly/releases/latest/download/pyfly-26.5.4-py3-none-any.whl"
```
### One-Line Install (CLI + Framework)
@@ -1136,7 +1136,12 @@ The git tag and human-readable display use the leading-zero form (`v26.05.01`);
See **[CHANGELOG.md](CHANGELOG.md)** for detailed release notes.
-**Current:** `v26.05.03` (2026-05-08) — Functional starters + Java/.NET parity:
+**Current:** `v26.05.04` (2026-05-08) — `pyfly.security` import-chain fix:
+
+- **Bug fix** — `pyfly.security/__init__.py` no longer eagerly imports a starlette-specific `SecurityMiddleware` that transitively pulls in `pyjwt`. Importing `pyfly` (and instantiating `PyFlyApplication`) now works without `[security]` extras installed. Optional symbols (`SecurityMiddleware`, `JWTService`, `BcryptPasswordEncoder`) only export when their underlying packages (`starlette`, `pyjwt`, `bcrypt`) are present. Regression test pinned in `tests/security/test_optional_imports.py`.
+- Verified: bare wheel install (`pip install pyfly`) now exposes `pyfly.domain` immediately; the `[web,cqrs,transactional,eventsourcing]` extras unblock the full application bootstrap path.
+
+**Previous:** `v26.05.03` (2026-05-08) — Functional starters + Java/.NET parity:
- **Starters now actually do something** — `@enable_*_stack` decorators no longer just set a marker attribute. They now inject their property defaults between framework defaults and the user's `pyfly.yaml`, so the bundle activates the modules it promises (`pyfly.cqrs.enabled`, `pyfly.transactional.enabled`, etc.) while explicit user values still win.
- **`@enable_web_stack` (new)** — dedicated web-tier starter for HTTP/REST APIs that don't need EDA, CQRS, or cache. Activates web framework adapter (Starlette/FastAPI), ASGI server, validation, actuator, observability, and resilience filters.
diff --git a/install.sh b/install.sh
index 48eb53a..c757fd2 100755
--- a/install.sh
+++ b/install.sh
@@ -26,7 +26,7 @@ set -euo pipefail
# ── Constants ──────────────────────────────────────────────────────────────────
-PYFLY_VERSION="26.05.03"
+PYFLY_VERSION="26.05.04"
PYFLY_REPO="https://github.com/fireflyframework/fireflyframework-pyfly.git"
DEFAULT_INSTALL_DIR="$HOME/.pyfly"
MIN_PYTHON_MAJOR=3
diff --git a/pyproject.toml b/pyproject.toml
index 266aaaa..820356a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,10 +4,10 @@ build-backend = "hatchling.build"
[project]
name = "pyfly"
-# CalVer YY.MM.PATCH — package metadata uses PEP 440 normalized form (26.5.3);
+# CalVer YY.MM.PATCH — package metadata uses PEP 440 normalized form (26.5.4);
# git tag, GitHub release and human-readable display use leading-zero form
-# (v26.05.03) to match the Java/.NET/Go siblings.
-version = "26.5.3"
+# (v26.05.04) to match the Java/.NET/Go siblings.
+version = "26.5.4"
description = "The official Python implementation of the Firefly Framework — DI, CQRS, EDA, hexagonal architecture, and more."
readme = "README.md"
license = "Apache-2.0"
diff --git a/src/pyfly/__init__.py b/src/pyfly/__init__.py
index eec3500..498687e 100644
--- a/src/pyfly/__init__.py
+++ b/src/pyfly/__init__.py
@@ -13,4 +13,4 @@
# limitations under the License.
"""PyFly — Enterprise Python Framework."""
-__version__ = "26.05.03"
+__version__ = "26.05.04"
diff --git a/src/pyfly/security/__init__.py b/src/pyfly/security/__init__.py
index 80089b2..d7323ea 100644
--- a/src/pyfly/security/__init__.py
+++ b/src/pyfly/security/__init__.py
@@ -11,26 +11,43 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
-"""PyFly Security — Authentication, authorization, and JWT integration."""
+"""PyFly Security — Authentication, authorization, and JWT integration.
+
+The starlette-specific :class:`SecurityMiddleware`, :class:`JWTService`
+and :class:`BcryptPasswordEncoder` are *optional* — they are only
+exported when their underlying third-party dependency
+(``starlette`` / ``pyjwt`` / ``bcrypt``) is installed. This keeps the
+security package importable from non-HTTP applications (workers, CLI
+tools, data jobs) that don't pull in the ``[web]`` / ``[security]``
+extras.
+"""
from pyfly.security.context import SecurityContext
from pyfly.security.decorators import secure
from pyfly.security.http_security import AccessRule, AccessRuleType, HttpSecurity, SecurityRule
from pyfly.security.method_security import post_authorize, pre_authorize
-from pyfly.security.middleware import SecurityMiddleware
__all__ = [
"AccessRule",
"AccessRuleType",
"HttpSecurity",
"SecurityContext",
- "SecurityMiddleware",
"SecurityRule",
"post_authorize",
"pre_authorize",
"secure",
]
+try:
+ from pyfly.security.middleware import SecurityMiddleware
+
+ __all__ += ["SecurityMiddleware"]
+except ImportError:
+ # ``pyfly.security.middleware`` re-exports a starlette adapter that
+ # transitively requires ``pyjwt``; skip it when those extras aren't
+ # installed.
+ pass
+
try:
from pyfly.security.jwt import JWTService
diff --git a/tests/security/test_optional_imports.py b/tests/security/test_optional_imports.py
new file mode 100644
index 0000000..189834c
--- /dev/null
+++ b/tests/security/test_optional_imports.py
@@ -0,0 +1,85 @@
+# Copyright 2026 Firefly Software Foundation.
+# Licensed under the Apache License, Version 2.0.
+"""Regression test: ``pyfly.security`` must import without infra extras.
+
+Before v26.05.04 ``pyfly.security.__init__`` unconditionally re-exported
+``SecurityMiddleware``, which transitively imported ``starlette`` and
+``jwt`` at module load time. That made ``import pyfly`` itself fail
+when those optional packages were missing — even for non-HTTP services
+that just want the kernel + DDD primitives.
+
+The fix wraps the ``SecurityMiddleware`` import in ``try / except
+ImportError`` so it's only exposed when its dependencies are
+satisfied. This test pins the behaviour by checking that the
+package's exports degrade gracefully.
+"""
+
+from __future__ import annotations
+
+import importlib
+
+import pytest
+
+
+def test_pyfly_security_imports_without_starlette_or_jwt() -> None:
+ """The package must import even if ``starlette`` / ``jwt`` are missing.
+
+ We can't easily uninstall packages mid-test, so this test merely
+ verifies that the ``__init__`` doesn't *unconditionally* depend on
+ them — the fix is to check that ``SecurityMiddleware`` is wrapped
+ in a ``try / except`` import block, and that the package can be
+ fully reloaded without crashing.
+ """
+ import pyfly.security as sec
+
+ # Reload to exercise the fresh import path again.
+ importlib.reload(sec)
+
+ # The unconditional symbols must always be available.
+ for name in (
+ "AccessRule",
+ "AccessRuleType",
+ "HttpSecurity",
+ "SecurityContext",
+ "SecurityRule",
+ "post_authorize",
+ "pre_authorize",
+ "secure",
+ ):
+ assert hasattr(sec, name), f"missing required symbol: {name}"
+
+
+def test_security_middleware_import_is_optional() -> None:
+ """Confirm the import is guarded — same source-shape used by
+ JWTService and BcryptPasswordEncoder, our reference patterns.
+ """
+ spec = importlib.util.find_spec("pyfly.security")
+ assert spec is not None
+ src = spec.origin
+ assert src is not None
+ with open(src) as fh:
+ body = fh.read()
+
+ # The eager import on the canonical symbol must be wrapped in a
+ # ``try`` block (matching the JWTService / password helpers).
+ assert "from pyfly.security.middleware import SecurityMiddleware" in body
+ middleware_idx = body.index("from pyfly.security.middleware import SecurityMiddleware")
+ preceding_try = body.rfind("try:", 0, middleware_idx)
+ assert preceding_try != -1, (
+ "SecurityMiddleware import must be wrapped in a try / except ImportError "
+ "block so the package stays importable without starlette/pyjwt."
+ )
+ # The matching except clause must appear after the import:
+ after = body[middleware_idx:]
+ assert "except ImportError" in after, "missing except ImportError after SecurityMiddleware import"
+
+
+@pytest.mark.parametrize(
+ "extra_symbol",
+ ["JWTService", "BcryptPasswordEncoder", "PasswordEncoder", "SecurityMiddleware"],
+)
+def test_optional_security_symbols_are_in_all_when_available(extra_symbol: str) -> None:
+ import pyfly.security as sec
+
+ if hasattr(sec, extra_symbol):
+ assert extra_symbol in sec.__all__