-
Notifications
You must be signed in to change notification settings - Fork 1
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.
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.
| 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
→ 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
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_runruns 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.
┌─────────── 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.
-
UUID PKs + audit columns on every entity via
IdMixin/TimestampMixin. -
ExternalRefMixinonOrganization,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 wrong —
lifecycle.py,costing.py,confidence.py,forecasting.pyare DB-free and unit-tested in isolation.
See Data-Model for the entities and Workflows for how the engines run.
SCM-Master — Hardware procurement & asset-lifecycle SCM with an AI decision layer · Repository · Built by Eugen Müller
Overview
Deep dives