This repository is a uv workspace (members = ["packages/*"]). The root is
a thin shell package, substack-gateway, whose source lives in
src/substack_gateway/:
app_factory.py,api_app.py,mcp_app.py: compose the Starlette app, the FastAPI/api/v1mount, and the FastMCP/mcpmount.registry.py: discovers every installed_rest/_mcpcapability at runtime through thesubstack_gateway.capabilitiesentry-point group.ext_loader.py,runtime.py: load and run extensions.
Functionality lives under packages/:
gateway_core/: shared foundation — auth, HTTP clients, config, capabilities, converters, models.gateway_oss/: aggregator — health routes, the extension protocol, backward-compat shims, versioning.gateway_<domain>/,gateway_<domain>_rest/,gateway_<domain>_mcp/: the service / REST / MCP trio for each domain (comments,following,notes,posts,profiles,drafts,stats).meis REST+MCP only, with no standalone service package.
Tests live per-package:
packages/<pkg>/tests/: pytest unit tests.packages/gateway_oss/features/: BDD (behave) integration tests that spin up the full Starlette app viaTestClientwithrespxfor HTTP mocking. Step definitions live infeatures/steps/; fixture data lives inpackages/gateway_oss/samples/.
Install dependencies (every workspace member, dev included):
uv sync --all-packages --devLint:
uv run ruff check .Formatting check:
uv run ruff format --check .Type checking:
uv run ty check .Build every workspace package:
uv build --all-packagesUnit tests (per package):
uv run pytest packages/gateway_oss/tests/
uv run pytest packages/gateway_drafts/tests/
uv run pytest packages/gateway_stats/tests/Or all of them at once:
uv run pytestBDD tests:
uv run behave packages/gateway_oss/features/Extensions can contribute:
- extra API routes
- extra app routes
- extra MCP tools
- lifespan hooks
- an MCP auth provider (only one may be active at a time)
- a credential resolver, for mapping an authenticated MCP caller to Substack credentials
- a cache (OSS ships no caching of its own — see
gateway_stats.cachefor the one cache-shaped extension point that exists today)
The runtime loads extensions from substack_gateway_oss.extensions
entry-points (installed packages) and from the GATEWAY_EXTENSION_MODULES
env var (comma-separated module:attr strings). An extension implements the
GatewayExtension protocol in packages/gateway_oss/src/gateway_oss/extensions/base.py.
All application settings are in gateway_core.config.Settings, using the
SUBSTACK_GATEWAY_ env prefix; gateway_oss.config re-exports it for
backward compatibility. Settings include:
SUBSTACK_GATEWAY_LOG_LEVELSUBSTACK_GATEWAY_SUBSTACK_BASE_URLSUBSTACK_GATEWAY_SUBSTACK_TIMEOUT_SECSUBSTACK_GATEWAY_SUBSTACK_CONNECT_TIMEOUT_SECSUBSTACK_GATEWAY_SUBSTACK_REQUESTS_PER_SECONDSUBSTACK_GATEWAY_SUBSTACK_MAX_CONNECTIONSSUBSTACK_GATEWAY_SUBSTACK_MAX_KEEPALIVE_CONNECTIONSSUBSTACK_GATEWAY_SUBSTACK_RETRY_ATTEMPTSSUBSTACK_GATEWAY_SUBSTACK_RETRY_MIN_WAIT_SECSUBSTACK_GATEWAY_SUBSTACK_RETRY_MAX_WAIT_SECSUBSTACK_GATEWAY_ADMIN_TOKEN(no default) — gates administrative surfaces an extension may expose. Nothing in this repository reads it, so leaving it unset costs a plain gateway nothing; an extension that does read it must refuse the request when it is unset rather than compare against nothing.SUBSTACK_GATEWAY_STATS_TIMESERIES_WATERMARK_LAG_DAYS(default2) — how many trailing days of publication analytics are treated as still maturing and re-fetched rather than reused
SUBSTACK_GATEWAY_REDIS_URL is not read by anything in this codebase —
caching was removed as a built-in feature in 4.0.0. An extension may install
its own cache and its own configuration for it.