Skip to content

Architecture

eugnmueller-87 edited this page Jun 24, 2026 · 1 revision

Architecture

SCM-Master is a single FastAPI application with a clean layered backend, a dependency-free static frontend, and a hexagonal port/adapter boundary for enterprise integration. The same code runs against SQLite (dev) and Postgres (prod); Alembic owns the schema.


Layered structure

                          HTTP  (JWT bearer, role-gated writes)
                            │
   frontend/  ──────────────┤  served at /  (dependency-free JS UI)
   (static)                 │
                            ▼
   app/api/v1/   one APIRouter per domain, mounted at /api/v1
   app/api/deps.py          get_db (per-request txn) + require_role
   app/api/errors.py        ServiceError → HTTP 404/409/422
                            │
                            ▼
   app/services/   business rules (the deciders)
     lifecycle · asset · provenance · sourcing · analytics · planning
     requisition · ordering · calibration · costing · tco · auth · forecasting
                            │
            ┌───────────────┼────────────────┐
            ▼               ▼                ▼
   app/agent/        app/integrations/   app/models/
   LLM copilot       hexagonal adapters  SQLAlchemy 2.0 ORM
   (advisory only)   (Coupa CSV + sync)  (typed Mapped[...])
                            │
                            ▼
   app/core/   config · db (engine, Base, mixins) · security (bcrypt+JWT)
               observability (JSON logs, request-id) · ratelimit · safety (forge-lock)
                            │
                            ▼
              SQLAlchemy 2.0  →  SQLite (dev) / Postgres 16 (prod)
              schema owned by  alembic/  (migrate-check gate in CI)

The hard architectural rule: the LLM (app/agent/) and the deciding services (app/services/) are separate layers. The agent proposes; services decide. The agent never imports a write path that bypasses a service guard.

Module map

Package Responsibility
app/core/config.py Pydantic settings (env / .env); is_production(), validate_production() boot guards; DB-URL driver auto-pinning
app/core/db.py Engine, session factory, Base, IdMixin (UUID PK), TimestampMixin (audit columns); pool_pre_ping + pool sizing
app/core/security.py bcrypt hashing (72-byte cap), HS256 JWT mint/verify
app/core/observability.py JSON structured logging, X-Request-ID correlation middleware
app/core/ratelimit.py In-process per-IP fixed-window limiter (login)
app/core/safety.py Forge-lock: refuses seeding/destructive ops when SCM_ENV=prod
app/models/ ORM: catalog, procurement, flow, requisition, costing, tco, decision, auth, ordering
app/schemas/ Pydantic Create/Update/Read per domain, decoupled from ORM
app/services/ CRUDService base + domain services holding all business rules
app/agent/ client (Claude), copilot, purchasing (the gate), confidence, grounding, signals, context, prompts, schemas
app/integrations/ base (port), coupa (adapter), sync (source-agnostic engine), schemas
app/api/v1/ Routers: auth, catalog, procurement, flow, asset, sourcing, planning, requisitions, agent, costing, tco, integrations, exports
app/seed*.py Deterministic synthetic data: seed, seed_demo, seed_history, seed_costing, seed_tco, seed_tracking

Request lifecycle

request
  → observability middleware  (assign X-Request-ID, start access-log line)
  → router (app/api/v1/<domain>)
  → deps: get_db()  opens a per-request transaction (commit on success, rollback on error)
  → deps: require_role(...)  decode JWT, check role for write endpoints
  → service method  (business rule; on write paths takes SELECT … FOR UPDATE on the hot row)
  → ORM  → SQLite / Postgres
  → ServiceError?  → app/api/errors.py maps to HTTP 404 / 409 / 422
  → response  (Pydantic Read schema)  + access-log line with request-id

The integration boundary (hexagonal)

Built so SCM-Master runs alongside an existing ERP/P2P landscape as the intelligence layer, not a replacement.

   upstream wire format            canonical records           existing domain services
   (Coupa PO export CSV)   ──►   FeedBatch (suppliers,   ──►   organization_service
        ▲                          materials, POs)              product_service
        │                              │                        purchase_order_service
   CoupaCsvAdapter                sync.sync_feed()              (upsert_by_external_ref)
   (app/integrations/coupa.py)    source-agnostic,             — NO rules duplicated —
                                  idempotent on
                                  (source_system, external_ref)
  • Adapters map an upstream format onto canonical records; the sync engine upserts them through the existing services, so no business rule is duplicated.
  • Idempotent on (source_system, external_ref) — re-importing updates in place, never duplicates.
  • A true dry_run runs the whole sync in a rolled-back SAVEPOINT and reports counts without persisting.
  • sap.py (IDoc/OData) and Coupa write-back (requisitions via an outbox) are the next, not-yet-built, steps.

Deployment topology

   ┌─────────── DEMO stack ───────────┐     ┌──────── PRODUCTION stack ────────┐
   │  scm-master  (Railway)           │     │  scm-master  (own Railway proj)   │
   │  + own Postgres                  │     │  + own Postgres                   │
   │  self-seeds on boot              │     │  SCM_ENV=prod  (forge-locked)     │
   │  SCM_ENV unset                   │     │  starts EMPTY — real data only    │
   └───────────────┬──────────────────┘     └────────────────┬──────────────────┘
                   │ analytics endpoints                      │ analytics endpoints
                   ▼                                          ▼
        SCM-POWER-BI cockpit (demo)                SCM-POWER-BI cockpit (prod)
        thin server-side proxy: logs in, pulls analytics, serves an exec dashboard

The two stacks share no database and cannot affect each other. The cockpit is a separate repo (SCM-POWER-BI). See Setup-and-Deployment.

Cross-cutting conventions

  • UUID PKs + audit columns on every entity via IdMixin / TimestampMixin.
  • ExternalRefMixin on Organization, Product, PurchaseOrder — the (source_system, external_ref) key for round-tripping synced records.
  • Decimal money in the costing/TCO engines (quantised to cents) so negotiation numbers are exact, not float-fuzzy.
  • Pure functions where it must never be wronglifecycle.py, costing.py, confidence.py, forecasting.py are DB-free and unit-tested in isolation.

See Data-Model for the entities and Workflows for how the engines run.

Clone this wiki locally