PacFully is a TypeScript monorepo: apps/web, apps/api, packages/shared, packaged as six Docker Compose services and fronted by Caddy on Hetzner osmrouter.
Monorepo layout
Repositories & packages
flowchart LR shared["packages/shared\ncatalog · constants · types"] web["apps/web\nNext.js 15 · R3F · editor"] api["apps/api\nFastify · Prisma · MinIO"] shared --> web shared --> api web -->|JWT / REST /api| api
TEMPLATE_CATALOGin shared drives galleries, mesh IDs, and API template responses.- Web owns all parametric geometry, dieline SVG/DXF, and WebGL capture.
- API owns users, projects, assets, API keys, events, imports.
Docker services
Compose topology (logical)
flowchart TB edge["Edge: Caddy\nTLS · pacfully.in"] nginx["nginx\n/ → web\n/api → api\n/files → MinIO"] web["web :3000"] api["api :4000"] pg["Postgres 16"] redis["Redis 7"] minio["MinIO"] edge --> nginx nginx --> web nginx --> api nginx --> minio api --> pg api --> redis api --> minio
Auth, projects, assets
Request data flow
sequenceDiagram participant B as Browser participant N as nginx participant A as Fastify API participant P as Postgres participant M as MinIO B->>N: POST /api/auth/login N->>A: /auth/login A->>P: verify user A-->>B: JWT B->>N: POST /api/projects (Bearer) N->>A: persist artworkJson A->>P: Project row B->>N: POST /api/assets multipart N->>A: store object A->>M: put object A->>P: Asset row A-->>B: /files/hash.ext
Prisma models: User, Project, Asset, ApiKey, Event, ImportedAsset. Notably there is no plan field on User yet.
Editor client architecture
- Zustand
useEditorStore— single snapshot for dimensions, materials, artworks, panel colors, undo stacks, view mode, panel collapse, plan preview. - R3F —
Viewport3D→PackagingModelregenerates meshes viagenerateModel. - Dieline SVG —
generateDieline→ interactiveDielineViewwith artwork clip paths. - Artwork pipeline — Canvas2D composite (package color underlay + texture) shared by 3D and dieline.
2D ↔ 3D sync
flowchart TB props["PropertiesPanel\nsetDimensions"] store["useEditorStore.snapshot"] mesh["PackagingModel\ngenerateModel"] die["DielineView\ngenerateDieline"] props --> store store --> mesh store --> die
Deployment topology (pacfully.in)
- Host: Hetzner
osmrouterat167.233.49.210 - App root:
/opt/pacfully - Edge:
osmrouter-caddy-1owns :80/:443; site block reverse-proxies topacfully-nginx-1:80 - Compose publish: nginx on
127.0.0.1:9080internally; db/redis/minio not public - Docs:
docs.pacfully.in→ static files under/opt/pacfully-docs(this site)
Production edge
flowchart LR user["User"] caddy["Caddy\nLet's Encrypt"] app["pacfully.in\n→ pacfully-nginx-1"] docs["docs.pacfully.in\n→ /opt/pacfully-docs"] user --> caddy caddy --> app caddy --> docs