Domain model¶
This page ships the conceptual model (UML class), the physical model (ER), and a glossary of each entity's attributes. Diagrams use Mermaid — rendered automatically by MkDocs Material.
Class diagram (UML)¶
classDiagram
class User {
+UUID id
+str email
+str password_hash
+str name
+str|None photo_url
+bool is_active
+datetime created_at
+signup() User
+login(password) tokens
}
class Organization {
+UUID id
+UUID owner_id
+str name
+str slug
+bool is_active
+datetime created_at
+invite(email, role) Invitation
+add_member(user, role) Membership
+transfer_ownership(target) void
}
class Membership {
+UUID id
+UUID organization_id
+UUID user_id
+Role role
+datetime joined_at
+can(permission) bool
}
class Invitation {
+UUID id
+UUID organization_id
+str email
+Role role
+str token_hash
+InvitationStatus status
+datetime expires_at
+accept(user) Membership
+revoke() void
}
class Product {
+UUID id
+UUID organization_id
+str title
+str description
+bool is_active
+list~str~ image_keys
+datetime created_at
}
class ProductVariant {
+UUID id
+UUID product_id
+str sku
+dict attributes
+int stock_balance() (derived)
+int current_price_cents() (derived)
}
class PriceHistory {
+UUID id
+UUID variant_id
+int amount_cents
+str currency
+datetime valid_from
}
class StockMovement {
+UUID id
+UUID variant_id
+StockKind kind
+int qty
+str reason
+str ref_type
+UUID ref_id
+datetime created_at
}
class Cart {
+UUID id
+UUID user_id
+UUID organization_id
+CartStatus status
+datetime expires_at
}
class CartItem {
+UUID id
+UUID cart_id
+UUID variant_id
+int qty
+int price_snapshot_cents
}
class Order {
+UUID id
+UUID buyer_id
+UUID organization_id
+OrderStatus status
+int total_cents
+str shipping_address
+str idempotency_key
+datetime created_at
+transition_to(status) void
}
class OrderItem {
+UUID id
+UUID order_id
+UUID variant_id
+int qty
+int unit_price_cents
}
class Review {
+UUID id
+UUID user_id
+UUID variant_id
+int score
+str comment
+datetime created_at
}
class PushSubscription {
+UUID id
+UUID user_id
+str endpoint
+str p256dh
+str auth
+datetime created_at
}
User "1" --o "0..2" Organization : owns
User "1" --o "0..5" Membership : has
Organization "1" --o "1..10" Membership : has
Organization "1" --o "*" Invitation : sends
Organization "1" --o "*" Product : sells
Product "1" --* "1..*" ProductVariant : composed of
ProductVariant "1" --o "*" PriceHistory : priced over time
ProductVariant "1" --o "*" StockMovement : ledgered by
User "1" --o "*" Cart : owns
Cart "1" --o "*" CartItem : holds
User "1" --o "*" Order : places
Order "1" --* "1..*" OrderItem : itemized as
User "1" --o "*" Review : writes
ProductVariant "1" --o "*" Review : reviewed by
User "1" --o "*" PushSubscription : registers
PushSubscription stores a device/browser the user subscribed to Web Push (endpoint + the p256dh/auth keys the browser generates). A user has N subscriptions (desktop, phone, each tab/profile). It's the target of the messaging background channel — the foreground channel (SSE) needs no persisted state, it's just a live connection per user_id.
ER diagram (physical model)¶
erDiagram
USERS ||--o{ ORGANIZATIONS : "owns"
USERS ||--o{ MEMBERSHIPS : "joins"
ORGANIZATIONS ||--o{ MEMBERSHIPS : "has"
ORGANIZATIONS ||--o{ INVITATIONS : "sends"
ORGANIZATIONS ||--o{ PRODUCTS : "sells"
PRODUCTS ||--|{ PRODUCT_VARIANTS : "has"
PRODUCT_VARIANTS ||--o{ PRICE_HISTORY : "priced"
PRODUCT_VARIANTS ||--o{ STOCK_MOVEMENTS : "tracked"
USERS ||--o{ CARTS : "owns"
CARTS ||--o{ CART_ITEMS : "holds"
CART_ITEMS }o--|| PRODUCT_VARIANTS : "refs"
USERS ||--o{ ORDERS : "places"
ORGANIZATIONS ||--o{ ORDERS : "fulfills"
ORDERS ||--|{ ORDER_ITEMS : "items"
ORDER_ITEMS }o--|| PRODUCT_VARIANTS : "refs"
USERS ||--o{ REVIEWS : "writes"
PRODUCT_VARIANTS ||--o{ REVIEWS : "rated"
USERS ||--o{ PUSH_SUBSCRIPTIONS : "registers"
USERS {
UUID id PK
string email UK
string password_hash
string name
string photo_url
bool is_active
datetime created_at
datetime updated_at
}
ORGANIZATIONS {
UUID id PK
UUID owner_id FK
string name
string slug UK
bool is_active
datetime created_at
datetime updated_at
}
MEMBERSHIPS {
UUID id PK
UUID organization_id FK
UUID user_id FK
string role
datetime joined_at
}
INVITATIONS {
UUID id PK
UUID organization_id FK
string email
string role
string token_hash UK
string status
datetime expires_at
datetime created_at
}
PRODUCTS {
UUID id PK
UUID organization_id FK
string title
string description
bool is_active
json image_keys
datetime created_at
datetime updated_at
}
PRODUCT_VARIANTS {
UUID id PK
UUID product_id FK
string sku UK
json attributes
datetime created_at
}
PRICE_HISTORY {
UUID id PK
UUID variant_id FK
int amount_cents
string currency
datetime valid_from
}
STOCK_MOVEMENTS {
UUID id PK
UUID variant_id FK
string kind
int qty
string reason
string ref_type
UUID ref_id
UUID audit_user_id
datetime created_at
}
CARTS {
UUID id PK
UUID user_id FK
UUID organization_id FK
string status
datetime expires_at
datetime created_at
}
CART_ITEMS {
UUID id PK
UUID cart_id FK
UUID variant_id FK
int qty
int price_snapshot_cents
}
ORDERS {
UUID id PK
UUID buyer_id FK
UUID organization_id FK
string status
int total_cents
string shipping_address
string idempotency_key UK
datetime created_at
datetime updated_at
}
ORDER_ITEMS {
UUID id PK
UUID order_id FK
UUID variant_id FK
int qty
int unit_price_cents
}
REVIEWS {
UUID id PK
UUID user_id FK
UUID variant_id FK
int score
string comment
datetime created_at
}
PUSH_SUBSCRIPTIONS {
UUID id PK
UUID user_id FK
string endpoint UK
string p256dh
string auth
datetime created_at
}
Enums¶
classDiagram
class Role {
<<enum>>
OWNER
ADMIN
MEMBER
}
class InvitationStatus {
<<enum>>
PENDING
ACCEPTED
REVOKED
EXPIRED
SUPERSEDED
}
class StockKind {
<<enum>>
IN
OUT
ADJUST
RESERVATION
RELEASE
}
class CartStatus {
<<enum>>
OPEN
CONVERTED
EXPIRED
ABANDONED
}
class OrderStatus {
<<enum>>
PENDING
PAID
SHIPPED
DELIVERED
CANCELLED
RETURNED
}
Invariants (executable summary)¶
| Entity | Invariant | Where it's enforced |
|---|---|---|
User |
unique email, active, bcrypt-hashed | DB constraint + signup service |
Organization |
owner is a member with role OWNER, unique slug, 1 OWNER per org | constraint + trigger or service |
Membership |
(org_id, user_id) unique, ≤10 per org, ≤5 per user, exactly 1 OWNER per org |
unique constraint + service guard |
Invitation |
unique token_hash, expires in 7d, terminal status doesn't mutate | constraint + state machine in service |
Product |
≥1 active variant to appear in the catalog | catalog query |
ProductVariant |
unique SKU within organization_id (via JOIN through product) |
composite constraint + service |
PriceHistory |
append-only — no UPDATE/DELETE | revoke DB permissions + service |
StockMovement |
append-only, balance never negative | service guard + check constraint |
Order |
unique idempotency_key, transitions follow the state machine |
constraint + state machine |
Entity → SDK primitive mapping¶
| Entity | Inherits from | Repository | Service base |
|---|---|---|---|
User |
BaseUserModel |
BaseRepository[UserModel] |
BaseService |
Organization |
BaseModel + AuditMixin + SoftDeleteMixin |
BaseRepository[OrganizationModel] |
BaseService |
Membership |
BaseModel + AuditMixin |
BaseRepository[MembershipModel] |
BaseService |
Invitation |
BaseModel + AuditMixin |
BaseRepository[InvitationModel] |
BaseService |
Product |
BaseModel + AuditMixin + SoftDeleteMixin |
custom (JOINs variant+price) | BaseService |
ProductVariant |
BaseModel + AuditMixin |
custom | BaseService |
PriceHistory |
BaseModel (append-only: updated_at exists but never changes) |
append-only | BaseService |
StockMovement |
BaseModel (append-only: updated_at exists but never changes) |
append-only via bulk_create_values when batching |
BaseService |
Cart/CartItem |
BaseModel + AuditMixin |
BaseRepository[CartModel] |
BaseService |
Order/OrderItem |
BaseModel + AuditMixin |
custom | BaseService |
Review |
BaseModel + AuditMixin |
BaseRepository[ReviewModel] |
BaseService |
PushSubscription |
SDK base subscription model (Web Push) | BaseRepository[PushSubscriptionModel] |
WebPushSubscriptionService |
PushSubscriptionModel is not hand-written — it concretizes the base subscription model the SDK ships (just declares __tablename__ and the user_id FK), exactly as in the Web Push recipe ». The WebPushSubscriptionService takes that BaseRepository[PushSubscriptionModel] + a WebPushDispatcher and handles CRUD and delivery (pruning dead 404/410 devices). There is no persisted Notification entity: each notification is ephemeral — it lives only as an SSE frame (foreground) and/or Web Push payload (background), both derived from the same domain event.
PriceHistory and StockMovement are append-only. Every BaseModel carries updated_at, and every BaseRepository carries update and delete — the SDK has no read-and-insert-only repository. Append-only here is a service rule: on these two tables it only calls add/list/get. To make the code unable to alter the history, give them a repository of their own that does not inherit BaseRepository and exposes only those three methods.
Next step¶
Jump to Critical flows to see the sequence diagrams covering signup, invitation, product creation, checkout, and shipment.