PacFully Docs

Solution architecture

PacFully is a TypeScript monorepo: apps/web, apps/api, packages/shared, packaged as six Docker Compose services and fronted by Caddy on Hetzner osmrouter.

Editor as the central client surface
The editor client is the heaviest surface — everything else feeds templates, auth, and persistence into it.

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
  

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
  
Tools index page reflecting product surface area
Marketing tools index — mirrors the breadth the architecture must support over time.

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.

Dashboard listing cloud-saved projects
Projects persist dimensions + artworkJson per user — cloud save from the editor toolbar.

Editor client architecture

2D ↔ 3D sync

flowchart TB
  props["PropertiesPanel\nsetDimensions"]
  store["useEditorStore.snapshot"]
  mesh["PackagingModel\ngenerateModel"]
  die["DielineView\ngenerateDieline"]
  props --> store
  store --> mesh
  store --> die
  
Split view proving shared state drives both panes
Split view is not two apps — both panes subscribe to the same Zustand snapshot.

Deployment topology (pacfully.in)

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
  
Admin console confirming production auth path
Admin console on the live host — proves JWT auth and API routing through Caddy → nginx → api.