AI Prompt: Part 4 — GridFlock Service + Slicer Container¶
Series: Forma3D.Connect Microservice Decomposition + GridFlock STL Pipeline (Part 4 of 6) Purpose: Build the GridFlock STL generation service and the BambuStudio slicer container — the core new business capability Estimated Effort: 22–28 hours Prerequisites: Parts 1–3 completed (shared libs, Gateway, Order/Print/Shipping services) Output:
apps/gridflock-service(STL generation, pipeline, feature flag, BullMQ processor) + Slicer Container (BambuStudio CLI with REST API), both building and passing tests Status: 🚧 TODO Previous Part: Part 3 — Print Service + Shipping Service Next Part: Part 5 — Order-GridFlock Integration + Docker/CI
🎯 Mission¶
Build the GridFlock Service and Slicer Container — the entirely new business capability. The GridFlock Service generates Gridfinity-compatible baseplates using JSCAD, slices them to Bambu Lab gcode via the Slicer Container, uploads gcode to SimplyPrint (via Print Service), and creates product mappings. Everything flows as in-memory buffers — no files touch disk.
What this part delivers:
- GridFlock Service (
apps/gridflock-service): - Per-tenant feature flag guard (using SystemConfig)
- External REST API (baseplates, plate sets, jobs, presets)
- Internal API (
/internal/gridflock/generate-for-order, mapping status) - BullMQ processor for async STL generation jobs
- Pipeline service: full STL → gcode → SimplyPrint → mapping flow
- Repository (tenant-isolated)
- Cleanup cron for expired jobs
- DTOs with validation
- Slicer Container:
- Dockerfile based on
linuxserver/bambustudio - Thin REST API wrapper (Node.js/Express)
POST /slice— receives STL buffer + profiles, returns gcodeGET /healthandGET /profiles- Bambu Lab A1 machine/process/filament profiles
- Prisma Schema — GridflockJob, GridflockPreset models + migration
- Seed Data — Feature flag, printer presets, default print settings
What this part does NOT do:
- Does NOT integrate with Shopify order flow (that's Part 5)
- Does NOT handle
gridflock.mapping-readyevents in Order Service (Part 5) - Does NOT update Docker Compose for production (Part 5)
- Does NOT update CI/CD pipeline (Part 5)
📌 Prerequisites (Parts 1–3 Completed)¶
Verify these before starting:
-
libs/gridflock-coreavailable (JSCAD generation, plate calculator, types) -
libs/service-commonavailable (event bus, internal auth, service clients, SlicerClient) - Gateway running on port 3000
- Order Service running on port 3001 with internal API
- Print Service running on port 3002 with SimplyPrint API Files upload endpoint
- All existing event flows working across services
🏗️ Architecture¶
GridFlock Domain¶
An entirely new business capability. When a Shopify order contains custom grid dimensions (width × height), this domain generates the 3D models (STL via JSCAD), slices them to printer-specific gcode (BambuStudio CLI), uploads the gcode to SimplyPrint (via Print Service), and creates a product mapping so subsequent orders for the same size can skip generation.
| Subdomain | Type | Responsibility |
|---|---|---|
| STL Generation | Core | JSCAD-based parametric grid generation (buffers, no files) |
| Slicing | Core | STL → gcode conversion via BambuStudio CLI container |
| Pipeline Orchestration | Core | Multi-step flow: generate → slice → upload → create mapping |
| Grid Configuration | Supporting | Presets, plate splitting, SKU normalization |
Why its own service? STL generation and slicing are CPU-intensive operations. Running these in the Order or Print service would block API responsiveness. This domain is gated by a per-tenant feature flag and may not run at all for non-GridFlock tenants.
SKU Naming Convention¶
GridFlock product mappings use a deterministic SKU:
GF-{largerMM}x{smallerMM}-{connector}-{magnets}
IMPORTANT: Dimensions are NORMALIZED — the larger dimension always comes first.
A 450×320mm grid and a 320×450mm grid produce the SAME SKU: GF-450x320-IP-MAG.
Connector codes: IP = Intersection Puzzle, EP = Edge Puzzle, NONE = No connectors
Magnet codes: MAG = With magnets, NOMAG = Without magnets
Slicer Container Architecture¶
BambuStudio CLI requires OpenGL/display infrastructure. The solution uses linuxserver/bambustudio with a thin Node.js REST wrapper:
┌──────────────────────────────────────────────────┐
│ Slicer Container (Docker) │
│ Based on linuxserver/bambustudio │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ Thin REST API (Node.js / Express) │ │
│ │ │ │
│ │ POST /slice │ │
│ │ ├─ Receives: STL buffer + profile paths │ │
│ │ ├─ Writes STL to temp dir, calls CLI │ │
│ │ ├─ Reads gcode output into buffer │ │
│ │ ├─ Cleans temp files │ │
│ │ └─ Returns: gcode/3MF buffer in response │ │
│ │ │ │
│ │ GET /health │ │
│ │ GET /profiles │ │
│ └───────────────────┬──────────────────────┘ │
│ │ │
│ ┌───────────────────▼──────────────────────┐ │
│ │ BambuStudio CLI Binary │ │
│ │ (with Xvfb/virtual display) │ │
│ │ │ │
│ │ ./bambu-studio --slice 0 │ │
│ │ --load-settings "machine;process" │ │
│ │ --load-filaments "filament.json" │ │
│ │ --export-3mf output.3mf input.stl │ │
│ └───────────────────────────────────────────┘ │
│ │
│ /profiles/ │
│ ├── bambu-a1/ │
│ │ ├── machine.json │
│ │ ├── process_020.json (0.20mm layer) │
│ │ ├── process_016.json (0.16mm layer) │
│ │ └── filament_pla.json │
│ ├── bambu-p1s/ │
│ │ └── ... │
│ └── bambu-x1c/ │
│ └── ... │
└──────────────────────────────────────────────────┘
Per-Tenant Print Settings¶
Print settings are stored in SystemConfig per tenant:
{
"printerModel": "bambu-lab-a1",
"bedSize": [256, 256],
"nozzleDiameter": 0.4,
"layerHeight": 0.20,
"filamentType": "PLA",
"infillPercentage": 15,
"wallLoops": 2,
"topLayers": 4,
"bottomLayers": 4,
"supportEnabled": false,
"machineProfilePath": "/profiles/bambu-a1/machine.json",
"processProfilePath": "/profiles/bambu-a1/process_020.json",
"filamentProfilePath": "/profiles/bambu-a1/filament_pla.json"
}
Product Mapping Invalidation¶
When a tenant's print settings change, all GridFlock product mappings for that tenant become invalid. The system must:
- Mark all GridFlock product mappings for the tenant as
invalidated - On next order for an invalidated mapping, regenerate (same flow as "not found")
📁 Files to Create¶
apps/gridflock-service¶
apps/gridflock-service/
├── src/
│ ├── main.ts
│ ├── app/
│ │ └── app.module.ts
│ ├── gridflock/
│ │ ├── gridflock.module.ts
│ │ ├── gridflock.controller.ts # /api/v1/gridflock/* endpoints
│ │ ├── gridflock.service.ts
│ │ ├── gridflock.repository.ts
│ │ ├── gridflock.processor.ts # BullMQ job processor
│ │ ├── gridflock-pipeline.service.ts # Full STL → gcode → SimplyPrint pipeline
│ │ ├── gridflock-cleanup.service.ts # Expired job cleanup cron
│ │ ├── dto/
│ │ │ ├── create-baseplate.dto.ts
│ │ │ ├── create-plate-set.dto.ts
│ │ │ ├── generate-for-order.dto.ts
│ │ │ ├── job-response.dto.ts
│ │ │ ├── job-status.dto.ts
│ │ │ └── preset.dto.ts
│ │ └── constants.ts
│ ├── guards/
│ │ └── gridflock-feature.guard.ts # Per-tenant feature flag check
│ ├── config/
│ ├── database/
│ ├── tenancy/
│ ├── common/
│ └── observability/
├── Dockerfile
├── project.json
└── jest.config.ts
Slicer Container¶
deployment/slicer/
├── Dockerfile # Based on linuxserver/bambustudio
├── api/
│ ├── server.js # Express REST API wrapper
│ ├── package.json
│ └── routes/
│ ├── slice.js # POST /slice
│ ├── health.js # GET /health
│ └── profiles.js # GET /profiles
├── profiles/
│ ├── bambu-a1/
│ │ ├── machine.json
│ │ ├── process_020.json
│ │ ├── process_016.json
│ │ └── filament_pla.json
│ ├── bambu-a1-mini/
│ │ └── ...
│ └── bambu-p1s/
│ └── ...
└── scripts/
└── entrypoint.sh
🔧 Implementation¶
Phase 6: GridFlock Service (16–20 hours)¶
Priority: High | Impact: High | Dependencies: Parts 1–3
6.1 Create GridFlock Service App¶
pnpm nx generate @nx/nest:application gridflock-service --directory=apps/gridflock-service
6.2 Prisma Schema Additions¶
Add to prisma/schema.prisma:
model GridflockJob {
id String @id @default(uuid())
tenantId String
tenant Tenant @relation(fields: [tenantId], references: [id])
type GridflockJobType
status GridflockJobStatus @default(QUEUED)
parameters Json
filePath String?
filename String?
fileSize Int?
progress Int @default(0)
error String?
createdAt DateTime @default(now())
startedAt DateTime?
completedAt DateTime?
expiresAt DateTime?
parentJobId String?
parentJob GridflockJob? @relation("PlateSetJobs", fields: [parentJobId], references: [id])
childJobs GridflockJob[] @relation("PlateSetJobs")
@@index([tenantId])
@@index([status])
@@index([createdAt])
@@index([expiresAt])
}
model GridflockPreset {
id String @id @default(uuid())
tenantId String?
tenant Tenant? @relation(fields: [tenantId], references: [id])
name String
description String?
parameters Json
isPublic Boolean @default(false)
isSystem Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([name, tenantId])
@@index([tenantId])
@@index([isPublic])
}
enum GridflockJobType {
BASEPLATE
PLATE_SET
}
enum GridflockJobStatus {
QUEUED
PROCESSING
COMPLETED
FAILED
EXPIRED
}
Run migration after schema changes.
6.3 Feature Flag Guard¶
// apps/gridflock-service/src/guards/gridflock-feature.guard.ts
@Injectable()
export class GridflockFeatureGuard implements CanActivate {
constructor(private readonly prisma: PrismaService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const tenantId = request.headers[USER_CONTEXT_HEADERS.TENANT_ID];
if (!tenantId) {
throw new ForbiddenException('No tenant context');
}
const config = await this.prisma.systemConfig.findUnique({
where: { key_tenantId: { key: 'gridflock.enabled', tenantId } },
});
if (!config || config.value !== 'true') {
throw new ForbiddenException('GridFlock STL generation is not enabled for this tenant.');
}
return true;
}
}
6.4 REST API Endpoints¶
External (via Gateway, gated by feature flag):
POST /api/v1/gridflock/baseplates — Queue async baseplate generation
POST /api/v1/gridflock/baseplates/sync — Generate small baseplate synchronously (STL only)
POST /api/v1/gridflock/plate-sets — Queue plate set (multiple plates)
GET /api/v1/gridflock/jobs/:id — Get job status
GET /api/v1/gridflock/jobs/:id/download — Download generated STL
GET /api/v1/gridflock/presets — List printer presets
GET /api/v1/gridflock/printer-profiles — List supported printers
Internal (service-to-service, gated by InternalAuthGuard):
POST /internal/gridflock/generate-for-order — Full pipeline: STL → gcode → SimplyPrint → mapping
GET /internal/gridflock/mapping-status/:sku — Check if a GridFlock SKU mapping exists
All external endpoints use @UseGuards(GridflockFeatureGuard).
6.5 The Generate-For-Order Pipeline (Core New Flow)¶
This is the key internal endpoint that the Order Service calls (in Part 5):
// apps/gridflock-service/src/gridflock/gridflock-pipeline.service.ts
@Injectable()
export class GridflockPipelineService {
constructor(
private readonly repository: GridflockRepository,
private readonly slicerClient: SlicerClient,
private readonly printServiceClient: PrintServiceClient,
private readonly orderServiceClient: OrderServiceClient,
private readonly eventBus: EventBusService,
private readonly logger: Logger,
) {}
/**
* BUFFER-BASED PIPELINE — No files written to disk at any point.
* Plates are processed SEQUENTIALLY to bound memory usage.
*/
async generateForOrder(request: GenerateForOrderDto): Promise<void> {
const { tenantId, orderId, lineItemId, widthMm, heightMm,
connectorType, magnets } = request;
// 1. Compute deterministic SKU
const sku = this.computeSku(widthMm, heightMm, connectorType, magnets);
// 2. Check if mapping already exists (race condition guard)
const existingMapping = await this.orderServiceClient.getProductMappingBySku(tenantId, sku);
if (existingMapping) {
await this.eventBus.publish({
eventType: SERVICE_EVENTS.GRIDFLOCK_MAPPING_READY,
tenantId, orderId, lineItemId, sku,
timestamp: new Date().toISOString(),
});
return;
}
// 3. Load tenant print settings
const printSettings = await this.loadTenantPrintSettings(tenantId);
// 4. Calculate plate set
const plateSet = calculatePlateSet({
targetGridSize: this.mmToGridUnits(widthMm, heightMm),
printerBedSize: printSettings.bedSize,
connectorType: this.mapConnectorType(connectorType),
connectorTolerance: 0.15,
magnets,
magnetDiameter: 6,
includeNumbering: true,
});
// 5–7. Process each plate SEQUENTIALLY
const uploadedFiles: Array<{
plateIndex: number;
filename: string;
simplyPrintFileId: string;
}> = [];
for (const plate of plateSet.plates) {
const stlFilename = `${sku}_plate${plate.index}.stl`;
const gcodeFilename = `${sku}_plate${plate.index}.3mf`;
// 5. Generate STL buffer in memory (JSCAD)
const stlResult = generateGridFlockPlate({
plateSize: plate.gridSize,
connectorIntersectionPuzzle: connectorType === 'intersection-puzzle',
connectorEdgePuzzle: connectorType === 'edge-puzzle',
magnets,
numbering: true,
numberIndex: plate.index,
});
// 6. Slice STL → gcode via HTTP to Slicer Container
const gcodeResult = await this.slicerClient.slice({
stlBuffer: stlResult.stlBuffer,
stlFilename,
machineProfile: printSettings.machineProfilePath,
processProfile: printSettings.processProfilePath,
filamentProfile: printSettings.filamentProfilePath,
});
// 7. Upload gcode buffer to SimplyPrint via Print Service
const uploaded = await this.printServiceClient.uploadFileToSimplyPrint(
tenantId,
{ filename: gcodeFilename, buffer: gcodeResult.gcodeBuffer },
);
uploadedFiles.push({
plateIndex: plate.index,
filename: gcodeFilename,
simplyPrintFileId: uploaded.simplyPrintFileId,
});
this.logger.log(
`Plate ${plate.index}/${plateSet.totalPlates} complete for ${sku}`,
);
}
// 8. Create product mapping (via Order Service)
await this.orderServiceClient.createProductMapping(tenantId, {
sku,
name: `GridFlock ${widthMm}×${heightMm}mm ${connectorType} ${magnets ? '+MAG' : ''}`,
isGridflock: true,
files: uploadedFiles.map(f => ({
simplyPrintFileId: f.simplyPrintFileId,
plateIndex: f.plateIndex,
filename: f.filename,
})),
});
// 9. Notify that mapping is ready
await this.eventBus.publish({
eventType: SERVICE_EVENTS.GRIDFLOCK_MAPPING_READY,
tenantId, orderId, lineItemId, sku,
timestamp: new Date().toISOString(),
});
}
private computeSku(widthMm: number, heightMm: number,
connectorType: string, magnets: boolean): string {
const dim1 = Math.max(widthMm, heightMm);
const dim2 = Math.min(widthMm, heightMm);
const connector = connectorType === 'intersection-puzzle' ? 'IP'
: connectorType === 'edge-puzzle' ? 'EP' : 'NONE';
const mag = magnets ? 'MAG' : 'NOMAG';
return `GF-${dim1}x${dim2}-${connector}-${mag}`;
}
private mmToGridUnits(widthMm: number, heightMm: number): [number, number] {
const GRID_SIZE = 42;
return [Math.floor(widthMm / GRID_SIZE), Math.floor(heightMm / GRID_SIZE)];
}
}
6.6 BullMQ Processor (for standalone STL requests)¶
The BullMQ processor handles external API requests (baseplates, plate sets) — NOT the order pipeline:
@Processor('gridflock-generation')
export class GridflockProcessor extends WorkerHost {
async process(job: Job<GenerationJobData>): Promise<void> {
const { jobId, tenantId, parameters } = job.data;
await this.repository.updateJobStatus(tenantId, jobId, 'PROCESSING');
const result = generateGridFlockPlate(parameters);
const filePath = path.join(this.stlOutputPath, tenantId, `${jobId}.stl`);
await fs.mkdir(path.dirname(filePath), { recursive: true });
await fs.writeFile(filePath, result.stlBuffer);
await this.repository.updateJobStatus(tenantId, jobId, 'COMPLETED', {
filePath, filename: result.filename, fileSize: result.fileSize,
completedAt: new Date(), expiresAt: addHours(new Date(), 24),
});
}
}
6.7 Seed Feature Flag, Presets, and Print Settings¶
Add to prisma/seed.ts:
// Enable GridFlock for default tenant
await prisma.systemConfig.upsert({
where: { key_tenantId: { key: 'gridflock.enabled', tenantId: defaultTenant.id } },
update: {},
create: {
key: 'gridflock.enabled',
value: 'true',
description: 'Enable GridFlock STL generation for this tenant',
tenantId: defaultTenant.id,
},
});
// Seed default print settings (Bambu Lab A1)
await prisma.systemConfig.upsert({
where: { key_tenantId: { key: 'gridflock.print_settings', tenantId: defaultTenant.id } },
update: {},
create: {
key: 'gridflock.print_settings',
value: JSON.stringify({
printerModel: 'bambu-lab-a1',
bedSize: [256, 256],
nozzleDiameter: 0.4,
layerHeight: 0.20,
filamentType: 'PLA',
infillPercentage: 15,
wallLoops: 2,
topLayers: 4,
bottomLayers: 4,
supportEnabled: false,
machineProfilePath: '/profiles/bambu-a1/machine.json',
processProfilePath: '/profiles/bambu-a1/process_020.json',
filamentProfilePath: '/profiles/bambu-a1/filament_pla.json',
}),
description: 'BambuStudio slicer settings for GridFlock',
tenantId: defaultTenant.id,
},
});
// Seed printer presets
const presets = [
{ name: 'Bambu Lab A1 Mini', description: '180×180mm bed', parameters: { bedSize: [180, 180], maxGridSize: [4, 4] } },
{ name: 'Bambu Lab A1 / P1S / X1C', description: '256×256mm bed', parameters: { bedSize: [256, 256], maxGridSize: [6, 6] } },
{ name: 'Prusa MK4', description: '250×210mm bed', parameters: { bedSize: [250, 210], maxGridSize: [5, 5] } },
{ name: 'Prusa Core One', description: '250×220mm bed', parameters: { bedSize: [250, 220], maxGridSize: [5, 5] } },
{ name: 'Prusa XL (Single)', description: '360×360mm bed', parameters: { bedSize: [360, 360], maxGridSize: [8, 8] } },
{ name: 'Creality Ender 3 V3 / S1', description: '220×220mm bed', parameters: { bedSize: [220, 220], maxGridSize: [5, 5] } },
{ name: 'Creality K1 Max', description: '300×300mm bed', parameters: { bedSize: [300, 300], maxGridSize: [7, 7] } },
{ name: 'Voron 2.4 (250)', description: '250×250mm bed', parameters: { bedSize: [250, 250], maxGridSize: [5, 5] } },
{ name: 'Voron 2.4 (350)', description: '350×350mm bed', parameters: { bedSize: [350, 350], maxGridSize: [8, 8] } },
];
for (const preset of presets) {
await prisma.gridflockPreset.upsert({
where: { name_tenantId: { name: preset.name, tenantId: null } },
update: { description: preset.description, parameters: preset.parameters },
create: { ...preset, isPublic: true, isSystem: true, tenantId: null },
});
}
Phase 7: Slicer Container (6–8 hours)¶
Priority: High | Impact: High | Dependencies: None (parallel with Phase 6)
7.1 Create Slicer Dockerfile¶
# deployment/slicer/Dockerfile
FROM linuxserver/bambustudio:latest AS base
# Install Node.js for the REST API wrapper
RUN apt-get update && apt-get install -y curl && \
curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
apt-get install -y nodejs && \
apt-get clean && rm -rf /var/lib/apt/lists/*
# Copy REST API
COPY api/ /app/api/
WORKDIR /app/api
RUN npm install --production
# Copy printer profiles
COPY profiles/ /profiles/
# Copy entrypoint
COPY scripts/entrypoint.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh
EXPOSE 3010
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD curl -f http://localhost:3010/health || exit 1
ENTRYPOINT ["/app/entrypoint.sh"]
7.2 REST API Wrapper¶
// deployment/slicer/api/server.js
const express = require('express');
const multer = require('multer');
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const os = require('os');
const app = express();
const upload = multer({ storage: multer.memoryStorage() });
app.post('/slice', upload.single('stl'), async (req, res) => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'slicer-'));
try {
const { machineProfile, processProfile, filamentProfile } = req.body;
const stlPath = path.join(tempDir, 'input.stl');
const outputPath = path.join(tempDir, 'output.3mf');
fs.writeFileSync(stlPath, req.file.buffer);
const cmd = `./bambu-studio --orient --arrange 1 ` +
`--load-settings "${machineProfile};${processProfile}" ` +
`--load-filaments "${filamentProfile}" ` +
`--slice 0 --export-3mf ${outputPath} ${stlPath}`;
execSync(cmd, { timeout: 120000 });
const gcodeBuffer = fs.readFileSync(outputPath);
res.set('Content-Type', 'application/octet-stream');
res.send(gcodeBuffer);
} catch (error) {
res.status(500).json({ error: error.message });
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
app.get('/health', (req, res) => {
res.json({ status: 'ok', slicer: 'bambustudio' });
});
app.get('/profiles', (req, res) => {
// List available profiles from /profiles/ directory
const profiles = {};
const profileDir = '/profiles';
for (const printer of fs.readdirSync(profileDir)) {
profiles[printer] = fs.readdirSync(path.join(profileDir, printer));
}
res.json(profiles);
});
app.listen(3010, () => console.log('Slicer API on port 3010'));
7.3 Configure Profiles¶
Create BambuStudio machine/process/filament profile JSON files for the Bambu Lab A1. These profiles should be extracted from BambuStudio's default profile set.
🧪 Testing Requirements¶
GridFlock Service Tests¶
- Feature flag guard allows/blocks per tenant
POST /api/v1/gridflock/baseplatesqueues async job (returns 202)POST /api/v1/gridflock/baseplates/syncreturns STL for small modelsPOST /api/v1/gridflock/baseplates/syncrejects large models (400)POST /api/v1/gridflock/plate-setsqueues plate set job (202)GET /api/v1/gridflock/jobs/:idreturns job statusGET /api/v1/gridflock/jobs/:id/downloadreturns STL file- All endpoints return 403 when feature flag is disabled
- All endpoints return 401 when not authenticated
- Pipeline service: full STL → slice → upload → mapping flow (mock slicer + print service)
- Pipeline with existing mapping: skips generation, publishes event immediately
- Pipeline error handling: slicer failure → publishes pipeline-failed event
- SKU computation and normalization
- Tenant print settings loaded from SystemConfig with defaults fallback
- BullMQ processor completes job lifecycle
- Cleanup cron removes expired jobs
- Repository is tenant-isolated
Slicer Container Tests¶
POST /sliceaccepts STL buffer + profile paths, returns gcodePOST /slicewith invalid STL returns meaningful errorGET /healthreturns statusGET /profilesreturns available profiles- Slicing with different profiles produces different output
- Timeout handling for large models
✅ Validation Checklist¶
Build & Lint¶
-
pnpm nx build gridflock-servicesucceeds -
pnpm nx run-many -t lint --allpasses - Prisma migration runs cleanly (GridFlock models added)
- Prisma seed completes (feature flag + presets + print settings)
GridFlock Service¶
- Runs on port 3004
- Feature flag is per-tenant (default tenant enabled)
- All external endpoints gated by feature flag
- Internal endpoints gated by InternalAuthGuard
- Pipeline processes plates sequentially (bounded memory)
- Pipeline is buffer-based — NO files written to disk
- BullMQ processor handles async jobs
Slicer Container¶
- Slicer container builds and starts
-
GET /healthreturns ok -
POST /sliceaccepts STL + profiles, returns gcode/3MF - BambuStudio CLI invoked with correct flags
- Bambu Lab A1 profiles correctly configured
- Temp files cleaned up after each slice
Pipeline (tested in isolation)¶
- STL generation → slicer call → SimplyPrint upload → mapping → event (with mocks)
- Existing mapping → skip generation → immediate event
- Pipeline failure → publishes
gridflock.pipeline-failedevent
🚫 Constraints¶
- GridFlock pipeline MUST use buffer-based processing — NO files written to disk
- Plates processed sequentially — only one plate's buffers in memory at a time
- Feature flag must gate ALL GridFlock endpoints per tenant
- Print settings must be per-tenant configurable via SystemConfig
- Port GridFlock geometry from GridFlock source
- Use Bambu Lab A1 (256×256mm bed) as standard for plate splitting
- No
any,ts-ignore, oreslint-disable
📚 Key References¶
- Feasibility study:
docs/03-architecture/research/gridflock-feasibility-study.md - GridFlock source: https://github.com/yawkat/gridflock
- JSCAD documentation: https://openjscad.xyz/
- BambuStudio CLI: https://github.com/bambulab/BambuStudio/wiki/Command-Line-Usage
- linuxserver/bambustudio: https://hub.docker.com/r/linuxserver/bambustudio
- SimplyPrint API Files: https://apidocs.simplyprint.io/#api-files
END OF PART 4
Previous: Part 3 — Print Service + Shipping Service Next: Part 5 — Order-GridFlock Integration + Docker/CI