Skip to content

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:

  1. GridFlock Service (apps/gridflock-service):
  2. Per-tenant feature flag guard (using SystemConfig)
  3. External REST API (baseplates, plate sets, jobs, presets)
  4. Internal API (/internal/gridflock/generate-for-order, mapping status)
  5. BullMQ processor for async STL generation jobs
  6. Pipeline service: full STL → gcode → SimplyPrint → mapping flow
  7. Repository (tenant-isolated)
  8. Cleanup cron for expired jobs
  9. DTOs with validation
  10. Slicer Container:
  11. Dockerfile based on linuxserver/bambustudio
  12. Thin REST API wrapper (Node.js/Express)
  13. POST /slice — receives STL buffer + profiles, returns gcode
  14. GET /health and GET /profiles
  15. Bambu Lab A1 machine/process/filament profiles
  16. Prisma Schema — GridflockJob, GridflockPreset models + migration
  17. 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-ready events 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-core available (JSCAD generation, plate calculator, types)
  • libs/service-common available (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:

  1. Mark all GridFlock product mappings for the tenant as invalidated
  2. 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/baseplates queues async job (returns 202)
  • POST /api/v1/gridflock/baseplates/sync returns STL for small models
  • POST /api/v1/gridflock/baseplates/sync rejects large models (400)
  • POST /api/v1/gridflock/plate-sets queues plate set job (202)
  • GET /api/v1/gridflock/jobs/:id returns job status
  • GET /api/v1/gridflock/jobs/:id/download returns 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 /slice accepts STL buffer + profile paths, returns gcode
  • POST /slice with invalid STL returns meaningful error
  • GET /health returns status
  • GET /profiles returns available profiles
  • Slicing with different profiles produces different output
  • Timeout handling for large models

✅ Validation Checklist

Build & Lint

  • pnpm nx build gridflock-service succeeds
  • pnpm nx run-many -t lint --all passes
  • 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 /health returns ok
  • POST /slice accepts 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-failed event

🚫 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, or eslint-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