Developer · Architecture overview
User Guide
Developer
Two apps, one contract, no mystery layers
PacFully is an npm-workspaces monorepo: a Next.js web app and a Fastify api that both compile against a shared types-and-constants package, plus plain Docker infrastructure. Everything below is the repository as it exists today.
The layers
apps/web — the product
Next.js 15 (App Router) · React 19 · Tailwind CSS v4 · zustand · react-three-fiber + drei + three
Everything the user touches: marketing pages, the galleries, the dashboard, and the /editor workspace. The editor renders parametric 3D meshes and SVG dielines client-side; account data is fetched from the api with a JWT stored in the browser.
apps/api — the account backend
Fastify 5 · Prisma → Postgres 16 · @fastify/jwt · @fastify/cors · @fastify/multipart
Auth (register/login/me), the public template catalog, user-scoped project CRUD, image asset uploads, developer API keys, and the key-authenticated /v1 surface. Every error is normalized to the shared ApiError JSON shape.
packages/shared — the contract
@pacfully/shared · TypeScript types + constants + the template catalog
One source of truth both apps compile against: User/Project/ApiKey/Asset/EditorState types, API_ROUTES path constants, request limits, the 10 template categories, and TEMPLATE_CATALOG — the 11 parametric template definitions that drive the gallery cards, the 3D generators, the dieline generators, and the api responses.
Infrastructure
Postgres 16 · Redis 7 · MinIO (S3-compatible) · nginx
Postgres stores users, projects, API keys, and asset records. Redis is provisioned for queues and caching. MinIO stores uploaded files in the Docker deployment. nginx is the single entrypoint in compose: / routes to web, /api/ to the api (prefix stripped), /files/ to MinIO.
Request flow, both topologies
In both topologies the browser calls the same-origin relative /api path. In local development the Next.js dev server rewrites it to the Fastify dev server on port 4000 (override with NEXT_PUBLIC_API_URL); in the Docker deployment the browser only ever talks to nginx, which fans out:
browser ──► http://localhost:3000 (Next.js dev server, pages + editor)
└─► /api/*, /files/* rewrites ──► http://localhost:4000 (Fastify api)
├─► Postgres 16 (docker compose service "db", localhost:5432)
└─► ./data/uploads (local-disk storage driver, served at /files/<name>)browser ──► http://localhost/ ──► web (Next.js production server)
http://localhost/api/* ──► api (Fastify; the /api prefix is stripped)
http://localhost/files/* ──► minio (S3-compatible object storage)
api ──► db (postgres:16-alpine, healthchecked)
└─► redis (redis:7-alpine — provisioned for queues/caching)File storage is a driver, not a decision
Uploads go through a StorageDriver interface (save/remove) selected by the STORAGE_DRIVER environment variable. The local driver writes content-addressed files — <sha256>.<ext> — under ./data/uploads and serves them from GET /files/:name. The minio driver (the compose default) puts the same content-addressed objects in the S3 bucket with anonymous read, and nginx serves /files/<name> straight from MinIO — the same URLs work in both worlds. Any S3-compatible store works by pointing the S3_* variables at it.
One error shape, everywhere
A single error handler in the api normalizes every failure — schema validation, JWT failures, oversized uploads, Prisma constraint errors, unexpected 5xx — into the shared ApiError shape: { statusCode, error, message }. If you script against the api, that is the only error JSON you will ever parse. The API reference documents it per endpoint.
What the api is not