PolePosition Architecture
This document explains how PolePosition is structured as a product and as a codebase.
For a visual overview, see the Architecture Diagram.
It is meant to help both humans and coding agents answer the same questions quickly:
- what the product does
- where CLI behavior lives
- where runtime helper APIs live
- where generated project behavior lives
- how
add moduleandremove modulesafely update files - which parts are stable conventions versus evolving surfaces
Product Shape
PolePosition is a project lifecycle CLI, not only a one-time scaffold tool or template repository.
Its value comes from connected lifecycle workflows:
polepos startpolepos add modulepolepos remove modulepolepos add integration ...polepos checkpolepos db ...
That means the product helps at:
- project creation
- codebase growth
- project contract checks
- schema lifecycle management
Templates are a delivery mechanism for those workflows, not the product boundary.
The generated project should stay FastAPI-native while still giving teams opinionated structure and defaults.
PolePosition also ships a small runtime helper package for application code:
from polepos.data import LRUCache, Trie
The import package polepos is intentionally separate from the internal
pole_position implementation package. Generated applications may import
polepos.data; they should not import pole_position.cli....
High-Level Flow
User command
-> CLI command handler
-> service layer
-> template copy/render and project patching
-> generated FastAPI project
More concretely:
polepos start myapp
-> startproject.py
-> project_creator.py
-> template_renderer.py
-> generated app under myapp/
polepos add module users
-> commands/add/module.py
-> module_creator/
-> module_templates/
-> generated module files + managed updates
polepos remove module users
-> commands/remove/module.py
-> module_remover/
-> removes generated module files + managed updates
polepos add integration kafka
-> commands/add/integration.py
-> integration_creator/
-> generated integration files + managed settings/dependency updates
polepos add integration rabbitmq
-> commands/add/integration.py
-> integration_creator/
-> generated integration files + managed settings/dependency updates
polepos db upgrade
-> commands/db/upgrade.py
-> db_runner.py
-> Alembic in generated project
polepos check
-> commands/check.py
-> project_checker/
-> core project diagnostics + added module lifecycle wiring + opt-in integration diagnostics
CLI Map
Main entry:
pole_position/cli/main.py
Important pieces:
command.py: command modelregistry.py: command registrycommands/: user-facing command handlersservices/: reusable implementation logic
Current command groups:
startadd moduleremove moduleadd integrationcheckdb statusdb upgradedb revisiondb downgradeupgradehelpversion
Template Map
The generated project template lives under:
pole_position/template/
This folder is copied first, then rendered with placeholder replacement.
Main placeholders include:
{{project_name}}{{project_import_name}}{{no_bytecode_command_prefix}}{{no_bytecode_readme_note}}
Rendering logic lives in:
pole_position/cli/services/template_renderer.py
Generated Project Shape
Generated apps use this layout:
src/<package>/
app.py
main.py
run.py
settings.py
auth/
bootstrap/
api/
db/
domain/
integrations/
modules/
The responsibilities are:
app.py: FastAPI application factory; it reads settings and configures logging whencreate_app()is calledmain.py: ASGI import target; it exposesapp = create_app()for Uvicornrun.py: local/runtime process entrypoint; it prints startup details and starts Uvicorn from settingsauth/: endpoint authentication foundationbootstrap/: logging, middleware, lifespan, error wiringapi/: API composition and shared dependenciesdb/: SQLAlchemy base, session, model import aggregationdomain/: domain exceptions and shared primitivesintegrations/: external systems such as LLM adaptersmodules/: feature modules such asstatus,users,customers
Runtime Entrypoints
Generated apps separate application construction from process startup:
app.py
-> create_app()
-> read settings
-> setup logging
-> build FastAPI app
main.py
-> app = create_app()
-> ASGI import target for Uvicorn
run.py
-> main()
-> read settings
-> print startup table
-> uvicorn.run("<package>.main:app", ...)
This separation is deliberate. Importing src/<package>/app.py should not
freeze .env values or configure logging by itself. Tests and dynamic runtime
overrides can import create_app, update environment values, clear the
get_settings() cache when needed, and then call create_app().
add module Architecture
polepos add module has two layers:
- template selection
- project patching
Template selection lives in:
pole_position/cli/services/module_templates/
Current templates:
standardcrudai-promptapi-only
Project patching lives in:
pole_position/cli/services/module_creator/
This package (a thin __init__.py facade over responsibility-focused
submodules such as preflight.py, files.py, wiring.py, and llm.py):
- writes module files
- writes generated tests
- updates shared registration points
- records the module template in
.poleposition.tomlwhen present - optionally adds LLM integration files
- optionally patches settings and
.env.example
Module routers use local paths. A generated standard module can define
@router.get("/") and @router.post("/") inside
src/<package>/modules/<name>/router.py; module_creator then registers the
router once in src/<package>/api/router.py with prefix="/<name>". The
FastAPI app applies the app-level API prefix in app.py, so a customers
module root route is served as /api/v1/customers/, not as a shared global /.
The --api-only CLI option is a shortcut for the api-only template. It
generates router, schemas, a module-local services/ package, and tests
without model, repository, or database model wiring.
The crud template stays database-backed like standard, but generates
detail, update, and delete routes plus CRUD-specific service and test names.
remove module Architecture
polepos remove module is the cleanup counterpart to add module.
Implementation lives in:
pole_position/cli/services/module_remover/
The remover detects the generated module template, then removes:
src/<package>/modules/<name>/- generated integration and unit tests for the detected template
- the module export from
modules/__init__.py - the API router import and include from
api/router.py - the model import from
db/models.pyfor database-backed generated modules - the module template entry from
.poleposition.tomlwhen present
It is a project-file operation, not a database operation. It does not open a database connection, create a migration, drop tables, delete data, or rewrite historical Alembic revisions. For database-backed modules, removing the model import narrows Alembic metadata discovery; a later reviewed migration decides whether the physical table is dropped, retained, or replaced.
For ai-prompt modules, removing the last AI prompt module also removes shared
LLM settings, .env.example values, and the integrations/llm scaffold. If
another AI prompt module remains, shared LLM files and settings stay in place.
The command checks managed markers, generated wiring, and generated module
content before deleting the module directory. If router, model, or export wiring
has drifted into an unsupported custom layout, it stops before removing files so
the project is not left partially cleaned. If module files or generated tests
appear to contain custom changes, it also stops unless --force is used. An
expected generated file that can no longer be decoded as text is treated as a
modified generated file, so --force can still remove the module intentionally
instead of failing during custom-change detection.
--trace reports the planned removals and updates without mutating files.
Router and model wiring checks are Python-aware. The remover can delete the
generated from <package>.modules.<name>.router ... import and matching
api_router.include_router(...) call, including multi-line generated calls. It
does not delete additional custom imports or includes for the same module. Those
custom references block removal until the user removes or rewires them.
--wiring-only is the escape hatch for customized module directories. It
removes PolePosition-managed exports, router wiring, database-backed module model
imports, and generated tests, but preserves the module directory and shared
integration scaffolds. This lets users detach generated wiring without deleting
custom code.
add integration Architecture
polepos add integration ... grows an existing project with external system
helpers while keeping the base template lean.
Current integrations:
kafkarabbitmqredisrq
External-system support is intentionally opt-in. Kafka writes
integrations/kafka helpers and adds aiokafka; RabbitMQ writes
integrations/rabbitmq helpers and adds aio-pika; Redis writes
integrations/redis helpers and adds redis; RQ writes integrations/rq
helpers and adds rq. These integrations patch settings and .env.example
values. Consumer loops and background workers are left as explicit worker/runtime
code instead of being started inside the FastAPI app process.
Integration env handling distinguishes active required keys from optional
commented examples. A required key that appears only as a comment is treated as
missing, so polepos add integration kafka inserts an active
KAFKA_BOOTSTRAP_SERVERS=... line even if # KAFKA_BOOTSTRAP_SERVERS=...
already exists. Optional examples such as # KAFKA_COMPRESSION_TYPE= and
# LLM_MAX_TOKENS= are allowed to remain commented.
Integration dependency patching targets [project].dependencies in
pyproject.toml. It tolerates normal TOML formatting differences such as
inline or multi-line arrays, but it does not patch dependency groups or
tool-specific dependency lists.
check Architecture
polepos check is the lifecycle contract validator. It keeps project
diagnostics separate from project mutation: it reads generated files and reports
drift, but it does not patch files, install packages, connect to databases, or
start external services.
Implementation lives in:
pole_position/cli/services/project_checker/
The command handler lives in:
pole_position/cli/commands/check.py
Current check layers:
- core checks: project identity, generated structure, Alembic config, and managed markers
- lifecycle checks: added module files, exports, router wiring, model wiring, and generated tests
- integration checks: Kafka, RabbitMQ, Redis, RQ, LLM, and auth workflow files, dependencies, settings keys, and env keys
Lifecycle orphan checks parse router and model Python files across the whole
file, not just lines before managed markers. This lets check report custom
imports that still reference a missing module after the generated block was
cleaned. Integration checks parse exact active setting/env keys so comments and
substring matches do not hide missing required values.
Core-only behavior is available through check_core_project() for internal
callers that need the foundation without lifecycle or integration checks.
The public check_project() path runs the full current contract.
New generated projects include .poleposition.toml at the project root. It is
small lifecycle metadata, not application configuration:
- application package name
- database mode:
sqlite,postgres,none, or user-managedcustom - generated module templates
- generated integration scaffolds
When the manifest is present, package detection, module template detection, and integration checks prefer it over inference. Older generated projects without the manifest continue to use structural inference.
CRUD feature options are encoded with the module template value, for example
customers = "crud[pagination,timestamps,tenant-scoped]". That keeps lifecycle
commands able to re-render the same generated variant when checking or removing
a module.
For the detailed user guide and agent-facing contract, see:
docs/project-checks.md
Managed Block Contract
add module and remove module depend on marker comments in generated files.
Current managed markers include:
# polepos:router-imports# polepos:router-includes# polepos:model-imports# polepos:module-exports# polepos:auth-settings# polepos:auth-env# polepos:integration-settings# polepos:integration-env# polepos:llm-settings# polepos:llm-env
These markers define PolePosition-managed regions.
Meaning:
- PolePosition may insert generated lines before these markers
- users may add surrounding code and comments
- users should not remove these markers unless they intend to manage those files manually
The region above a marker is PolePosition-controlled
For the import- and export-style markers (router-imports, model-imports,
module-exports), PolePosition keeps the generated lines directly above the
marker in alphabetical order, so repeated add module runs produce a stable,
diff-friendly block instead of append-only churn.
The trade-off: that region is managed. A line you add by hand there that matches
the generated pattern (for example another
from <package>.modules.<name> import router import placed above
# polepos:router-imports) is treated as part of the managed block and may be
re-sorted on the next add module run. This is expected behavior, not a bug.
To keep a custom line exactly where you put it, make sure it does not look like a generated entry: place it in a clearly separate region (for example below the marker, or grouped with your other non-generated imports) rather than interleaved with the generated lines.
The most important managed files are:
src/<package>/api/router.pysrc/<package>/db/models.pysrc/<package>/modules/__init__.pysrc/<package>/settings.py.env.example.poleposition.toml
If these markers are removed or rearranged incorrectly, polepos add module,
polepos remove module, or polepos add integration ... may fail or stop
updating the file automatically.
polepos check --fix can restore missing safe markers. For api/router.py,
the fixer treats api_router.include_router(...) as Python syntax and places
# polepos:router-includes after the complete statement, including multi-line
router include calls.
Lifecycle Manifest and Dependency Patching
Beyond managed markers, two services give the lifecycle commands a reliable view of project state without re-deriving everything from files on each run.
Lifecycle manifest (.poleposition.toml)
pole_position/cli/services/project_manifest.py owns the per-project manifest
written as .poleposition.toml at the project root. It records what the
generated project is, so add, remove, and check do not rely only on
inference:
package_name: the application package recorded atstartdatabase:sqlite,postgres, ornone[modules]: each added module's template, e.g.customers = "crud[pagination,timestamps]"[integrations]: generated integrations askafka = true
The ProjectManifest dataclass also carries invalid_integrations and
read_error, so check can report a malformed manifest instead of crashing.
Module template values are encoded and decoded with
format_manifest_module_template / parse_manifest_module_template (the
crud[...] suffix carries the opt-in CRUD feature set). Mutations go through
record_manifest_module / remove_manifest_module and
record_manifest_integration / remove_manifest_integration, which keep the
file stable and comment-tolerant.
Dependency patching
Adding auth or an integration may require a dependency in the generated
project's pyproject.toml. This is split into a pure contract layer and a
file-editing layer:
dependency_contract.py: parsing and comparison only.DependencyEntrypreserves a line's indent, quote style, value, and trailing text, anddependency_contract_satisfied(dependencies, required)returns whether an existing dependency already covers the required name, extras, and minimum version. No file I/O.pyproject_editor.py: the editor.ensure_project_dependency(path, dependency)finds the[project]dependenciesarray (inline or multi-line) and replaces or appends the entry while preserving the file's existing formatting.
Because patching is contract-driven it is idempotent: a dependency that already
satisfies the contract is left untouched, so re-running add does not create
duplicates.
Database Lifecycle
PolePosition is migration-first.
Generated projects include:
- Alembic
- SQLAlchemy
db/models.pyas the import aggregation point
This means:
- schema changes should go through Alembic
- app startup should not recreate schema ad hoc
- new database-backed modules must be wired into
db/models.py
The main lifecycle is:
polepos start
-> edit models
-> polepos db revision -m "..."
-> polepos db upgrade
The remove lifecycle keeps the same separation:
polepos remove module customers
-> review whether database tables should remain
-> polepos db revision -m "remove customers table"
-> polepos db upgrade
If the team wants to remove only API code while retaining data, it should stop after the remove command and avoid generating a drop-table migration.
Authentication Boundary
Generated projects now include a JWT-based authentication foundation.
The important separation is:
- authentication: who is calling?
- authorization: what can this user access?
The generated auth package provides token helpers, get_current_user, and
require_roles(...) so newly added modules can define protected and role-gated
routes explicitly.
This is meant as a reusable route-boundary pattern, not a full identity system yet.
Logging Boundary
Generated projects support:
LOG_FORMAT=textLOG_FORMAT=json
Logging is configured centrally in:
src/<package>/bootstrap/logging.py
The generated logging contract is:
get_logger(__name__)is the preferred entrypoint- logging setup happens from
create_app(), not atapp.pyimport time - text logs are good for development
- JSON logs are good for production pipelines
Examples Map
Concrete scenario guides live under:
examples/
Current examples:
examples/auth-foundation/examples/html-swap/examples/kafka-quick-start/examples/rabbitmq-quick-start/examples/redis-cache/examples/openai-prompt/
Use these when you want to understand how the generated project should be reshaped for a real use case instead of reading only the raw template files.
How To Read This Repo Efficiently
If you are new to the repository, the shortest reliable reading path is:
README.mdAGENTS.md- this file
pole_position/cli/main.pypole_position/cli/commands/pole_position/cli/services/pole_position/template/pole_position/tests/examples/
This order helps because it moves from product intent to command flow to generated runtime behavior.