Project
On-Demand 3D Printer Platform
API and web application for intake, quoting, queue management, and fulfillment across custom print requests.
Overview
This project is meant to turn a pile of manual print-request coordination into a usable system. The API handles request intake and print-job state. The web app handles quoting, operations visibility, and customer communication.
Current milestone
Build a dependable order-to-queue path that keeps pricing, material selection, and production status aligned.
Roadmap
- Define the request and quote lifecycle clearly
- Add authenticated operator workflows for reviewing jobs
- Track printer capacity and queue contention
- Generate customer-visible status updates
- Add webhook-driven notifications for state changes
Recent progress
2026-06-12 · Use Slant3D V2 draft orders for cart shipping estimates (#154)
/cart/shipping now estimates shipping via Slant3D V2 draft orders instead of the V1 order/estimate flow. The route builds a V2 order payload from cart items plus the authenticated user’s shipping profile, while keeping the existing caller-facing response shape.
-
Integration change
- Replace V1
order/estimatewith V2POST /orders - Stop short of any follow-up
POST /orders/{publicOrderId}call - Normalize V2 estimate responses back to
{ shippingCost }
- Replace V1
-
Order payload construction
- Source print items from cart rows joined to product metadata
- Require
products.publicFileServiceIdper item - Use cart
filamentIdwhen present; otherwise fall back to PLA Black - Carry shipping/billing address data from the authenticated user profile
{
shippingAddress: { ...profileAddress },
billingAddress: { ...profileAddress },
orderItems: [
{
publicFileServiceId,
filamentId,
quantity,
},
],
}
Impact: - API behavior
-
Existing
/cart/shippingcallers still receive{ shippingCost } -
Missing
publicFileServiceIdnow returns a clear 400-style response -
Slant3D draft-order failures now return a clear upstream failure response with status/details
-
Operationally
- Shipping estimation now depends on V2 draft-order semantics and
publicFileServiceIdbeing populated on products
- Shipping estimation now depends on V2 draft-order semantics and
Context: paths src, test
2026-06-12 · Add cart filamentId support for Slant3D V2 fulfillment (#153)
Cart items now carry an optional Slant3D V2 filamentId while preserving existing color and filamentType behavior. The cart API also normalizes missing filament IDs to the default PLA Black filament so legacy and new callers behave consistently.
-
Schema + persistence
- Added cart-level
filamentIdstorage. - Added a D1 migration for the new
filament_idcolumn. - Reused a shared PLA Black default filament ID constant.
- Added cart-level
-
API behavior
POST /cart/addaccepts optionalfilamentIdand validates it as a UUID.- Missing
filamentIdis defaulted to PLA Black. - Cart item reads include
filamentId. - Existing cart matching/backcompat still respects
colorandfilamentType; legacy rows withoutfilamentIdare treated as default PLA Black.
-
Coverage
- Added focused cart tests for explicit filament preservation, UUID validation, and default filament behavior.
{
cartId,
skuNumber,
quantity,
color,
filamentType,
filamentId: '76fe1f79-3f1e-43e4-b8f4-61159de5b93c' // defaulted when omitted
}
Impact: Cart callers can now send and retrieve Slant3D filament public IDs for V2 fulfillment. Callers that only use color and filamentType continue to work unchanged, and carts without a supplied filament ID resolve to PLA Black.
Context: paths drizzle, src, test
2026-06-12 · Stop /v2/upload from self-fetching protected V2 routes (#152)
/v2/upload was routing through protected local Worker endpoints for presign, confirm, and estimate, which broke the authenticated upload flow unless those internal self-fetches succeeded. This change moves the Slant3D V2 file operations into shared helpers so /v2/upload calls Slant directly while the public helper endpoints keep their existing response contracts.
-
Shared Slant3D V2 file helpers
- Extract direct-upload, confirm-upload, and estimate calls into
src/lib/slant3d-v2-files.ts - Centralize request construction and error parsing for the V2 file workflow
parseResponseDetailstriesresponse.json()first, then falls back totext()+JSON.parseto handle varied Slant3D error response shapes
- Extract direct-upload, confirm-upload, and estimate calls into
-
/v2/uploadflow- Replace internal fetches to
/v2/presigned-upload,/v2/confirm, and/v2/estimate - Preserve current STL validation behavior and
/v2/uploadresponse shape - Keep the same default estimate behavior for PLA BLACK / quantity 1
- Replace internal fetches to
-
Protected route compatibility
- Rewire
/v2/presigned-upload,/v2/confirm, and/v2/estimateto use the same shared helpers - Add malformed-response validation in each route handler (missing
total,presignedUrl/key, orpublicFileServiceId/name/fileURL) to preserve 500 error contracts - Adopt estimate response normalization from main, filling in fallback values for
publicFileServiceId,quantity,filamentId, andestimatedCost - Preserve their public JSON contracts instead of introducing a second code path
- Rewire
const presigned = await createSlant3DDirectUpload(c.env, {
name: fileName.replace(/\.stl$/i, ''),
ownerId: userId?.toString() || 'anonymous',
});
const confirmed = await confirmSlant3DUpload(c.env, presigned.filePlaceholder);
const estimate = await estimateSlant3DFile(c.env, confirmed.publicFileServiceId, {
filamentId: defaultFilamentId,
quantity: 1,
});
Impact: - /v2/upload no longer depends on unauthenticated self-fetches to protected local routes
- Authenticated uploads continue returning the same payload, but now complete through direct Slant3D helper calls
/v2/estimateresponse now includes normalized fields (estimatedCost, fallbackquantity,filamentId,publicFileServiceId) for more consistent downstream consumption- Coverage now explicitly includes invalid type, empty file, oversized file, Slant failures, success, and the absence of local protected endpoint self-fetching
Context: paths src, test
2026-06-12 · Align Slant3D V2 file routes with live file API contract (#151)
Aligned the Slant3D V2 file flow with the live contract used by the printer routes. /v2/presigned-upload, /v2/confirm, and /v2/estimate now follow the live file endpoints and treat estimate totals as data.total.
-
Route contract alignment
- kept direct upload on
POST /files/direct-upload - kept confirm on
POST /files/confirm-upload - kept estimate on
POST /files/{publicFileId}/estimate
- kept direct upload on
-
Response shape normalization
- validated successful Slant responses before using them
- normalized local estimate responses from Slant
data.total - preserved local
estimatedCostas a compatibility alias for existing callers
-
Failure handling
- return explicit errors when Slant returns malformed successful payloads
- tightened add-product estimate parsing to require the live estimate shape instead of falling back across legacy fields
const total = estimateResponse.data.total;
return {
success: true,
data: {
total,
estimatedCost: total, // compatibility
},
};
Impact: Users and operators get more predictable failures when Slant returns malformed payloads instead of partial/implicit parsing. Estimate-backed flows now consistently price from the live Slant V2 total field while keeping existing local response consumers compatible.
Context: paths src, test
2026-06-12 · Remove duplicate mounted Stripe webhook route (#156)
Removed the stale duplicate Stripe webhook route so the mounted app has a single authoritative /webhook/stripe implementation in src/routes/payments.ts. This cleanup also removes the obsolete TODO-only handler path and adds a regression check around the mounted webhook behavior.
-
Route ownership
- Removed the standalone
src/routes/webhooks.tsimplementation. - Stopped mounting the duplicate webhook router from
src/index.ts. - Kept
src/routes/payments.tsas the sole mounted owner of/webhook/stripe.
- Removed the standalone
-
Behavior guardrail
- Added a focused payments-route test covering an unhandled Stripe event to assert the app resolves through the payments webhook handler, not the deleted stale route.
.route('/', paymentsRouter)
.route('/', shoppingCart);
Impact: - Stripe webhooks now resolve through one mounted handler only, eliminating ambiguity in request handling and OpenAPI ownership.
- Operators should configure Stripe against the existing
/webhook/stripeendpoint owned by payments.
Context: paths src, test
2026-06-11 · Remove legacy PayPal payment surface (#150)
-
API surface
- Removed
POST /paypalfrom payments routing. - Deleted the PayPal access helper and removed PayPal response types.
- Removed
-
Dependencies / test scaffolding
- Removed
@paypal/checkout-server-sdkfrompackage.jsonandpnpm-lock.yaml. - Removed PayPal mocks and skipped PayPal route tests.
- Added a focused assertion that the legacy route is no longer exposed.
- Removed
const res = await app.fetch(new Request('http://localhost/paypal', {
method: 'POST',
}), env);
expect(res.status).toBe(404);
- Package management cleanup
- Removed the tracked
package-lock.jsonfile so the repo only keeps pnpm lock metadata.
- Removed the tracked
Impact: - Stripe remains unchanged.
POST /paypalis no longer available; callers now receive404.- PayPal env/config is no longer required by app code.
- Maintainers should use pnpm lockfiles only;
package-lock.jsonis no longer tracked.
Context: paths package-lock.json, package.json, pnpm-lock.yaml, src, test
2026-06-11 · Bump vite from 6.3.4 to 6.4.2 (#141)
Bumps vite from 6.3.4 to 6.4.2.
Impact: See the linked pull request for implementation details.
Context: labels dependencies, javascript | paths package.json, pnpm-lock.yaml
2026-06-11 · new workflow for blog setup (#128)
Adds the project-notes automation workflow set for this repository (PR-note validation, markdown generation, and blog-sync flow), plus follow-up fixes from review feedback.
Also reconciles merge conflicts with main so the Node CI workflow keeps running both the standard test suite and test:project-notes with a frozen lockfile install.
Impact: No direct end-user product behavior changes.
Maintainers get updated CI/workflow behavior for project-notes automation and a conflict-free branch aligned with current main test infrastructure.
Context: paths .github, README.md, package.json, project-notes.config.json, test, tools
2026-06-11 · fixed tests (#129)
fixed tests
Impact: See the linked pull request for implementation details.
Context: paths .github, package.json, pnpm-lock.yaml, test, worker-configuration.d.ts
2026-03-18 · Revert "Bump wrangler from 4.35.0 to 4.75.0" (#127)
Reverts benhalverson/3dprinter-farm#126
Impact: See the linked pull request for implementation details.
Context: paths pnpm-lock.yaml
Notes
- 2026-06-12: - Compatibility - The route intentionally preserves the old outward response contract while adapting to a different upstream API shape - Fallback behavior - Filament defaults to PLA Black until cart-level
filamentIdis reliably available everywhere - 2026-06-12: Legacy cart rows may not have
filament_id; the cart layer treats those as PLA Black and backfills that default during add/merge flows to keep behavior stable across pre- and post-migration data. - 2026-06-12: - The V2 file workflow was duplicated across route handlers; extracting shared helpers removes the internal routing dependency without changing the external helper-route contracts -
estimatecost fallback handling remains in place because the current Slant response shape is not fully uniform across callers - Spying onFile.sizedoes not survive Cloudflare Workers multipart serialisation in the vitest pool — the oversized-file test requires an actualUint8Arrayallocation to cross the 100 MB threshold reliably -parseResponseDetailsmust attemptjson()beforetext()because different Slant3D error responses (and test mocks) surface the body through different methods; falling back through both with aJSON.parseattempt handles all cases - 2026-06-12: The live Slant V2 estimate contract should be treated as
data.total, not a mix of legacy top-level or alternate cost fields. The local/v2/estimateroute intentionally continues to exposeestimatedCostalongsidetotalto avoid breaking downstream callers during the transition. - 2026-06-12: - The deleted webhook route had diverged into a TODO-only implementation with different response semantics; keeping a single mounted handler avoids silent drift between duplicate webhook paths.
- 2026-06-11: - This repository uses pnpm, so npm lockfiles should not be committed or updated here.