Skip to content

Domain Model

PrintStudio’s domain model is defined in packages/domain as 408 Zod schemas. The database schema mirrors these types via Drizzle ORM in packages/core/src/db/schema/.

Customer
└── Order (1:N)
└── LineItem (1:N)
└── Job (1:1)
├── SKU (N:1)
├── Printer (N:1)
└── StateTransition (1:N)
Printer
└── PrinterStatus (polling)
Material
└── InventoryRecord (1:N)
SKU
├── Material (N:1)
└── SlicerProfile (N:1)
type Customer = {
id: string;
email: string;
name: string;
role: "customer" | "operator" | "admin";
stripeCustomerId?: string;
createdAt: Date;
};
type Order = {
id: string; // e.g., "ord-abc123"
customerId: string;
status: OrderStatus; // pending_payment | paid | processing | ...
lineItems: LineItem[];
subtotal: number;
shipping: number;
tax: number;
total: number;
stripeSessionId?: string;
stripePaymentIntentId?: string;
shippingAddress: Address;
notes?: string;
createdAt: Date;
updatedAt: Date;
};
type Job = {
id: string; // e.g., "job-uuid"
orderId: string;
lineItemId: string;
skuId: string;
status: JobStatus; // CREATED | VALIDATED | SLICED | QUEUED | ...
printerId?: string; // assigned printer
gcodeFile?: string; // path in file storage
estimatedPrintTime: number; // minutes
actualPrintTime?: number;
estimatedMaterialGrams: number;
actualMaterialGrams?: number;
retryCount: number;
lastError?: string;
preferredPrinterId?: string; // operator override
priority: number; // higher = earlier in queue
createdAt: Date;
updatedAt: Date;
};
type SKU = {
id: string;
name: string;
description: string;
machineType: MachineType;
material: string;
estimatedPrintTime: number;
estimatedMaterialGrams: number;
basePrice: number;
qualityTiers: Record<QualityTier, { multiplier: number; slicerProfile?: string }>;
options: SKUOption[];
slicerProfile: string;
active: boolean;
};
type Printer = {
id: string;
name: string;
type: "moonraker" | "octoprint" | "bambu";
machineType: MachineType;
baseUrl: string;
apiKey?: string;
buildVolume: { x: number; y: number; z: number };
status: "online" | "offline" | "printing" | "paused" | "error" | "maintenance";
currentJobId?: string;
amsSlots?: AMSSlot[];
createdAt: Date;
};
type JobStatus =
| "CREATED"
| "VALIDATED"
| "SLICED"
| "QUEUED"
| "PRINTING"
| "POST_PROCESS"
| "READY"
| "SHIPPED"
| "FAILED"
| "CANCELLED";
type MachineType =
| "fdm_large" // Bambu X1C, Voron 2.4, Kobra Max
| "fdm_standard" // Prusa MK4, Ender 3
| "resin" // Elegoo Saturn, Phrozen
| "laser"; // Glowforge, xTool
type QualityTier = "draft" | "standard" | "fine";

Tables are defined with Drizzle ORM in packages/core/src/db/schema/:

TableKey relationships
customersroot entity
orders→ customers
line_items→ orders, → skus
jobs→ line_items, → printers
job_transitions→ jobs (audit log)
printersroot entity
printer_status→ printers (latest poll)
skusroot entity
materialsroot entity
inventory_records→ materials
api_keys→ customers
sessions→ customers

All input validation uses Zod schemas from packages/domain. The API layer validates every request body against these schemas before passing to business logic. Invalid input returns 400 with Zod’s error details:

{
"error": "Validation failed",
"issues": [
{
"path": ["lineItems", 0, "quantity"],
"message": "Expected number, received string"
}
]
}