Architecture
Pelagia Portal is a full-stack Next.js 15 (App Router) application with Prisma + PostgreSQL, NextAuth v5 auth, and Tailwind CSS v4. It is an internal line-of-business app; the stack optimises for developer velocity, end-to-end type safety, and operational simplicity (minimal infrastructure).
Technology stack
| Layer | Choice | Why |
|---|---|---|
| Framework | Next.js 15 (App Router) | Full-stack React; Server Components cut client JS; built-in API routes |
| Language | TypeScript 5 (strict) | Shared types front-to-back; contract mismatches caught at compile time |
| UI | React 19 | Concurrent rendering, Server Components |
| Components | shadcn/ui + Radix primitives | Accessible, unstyled, copy-owned source (no black-box upgrades) |
| Styling | Tailwind CSS v4 | Utility-first, consistent design tokens |
| ORM | Prisma 5 | Type-safe client; schema-first migrations; Studio for data inspection |
| Database | PostgreSQL 16 | ACID; JSON columns; mature tooling |
| Auth | NextAuth.js v5 | Microsoft Entra SSO and credentials provider |
| File storage | Cloudflare R2 (prod) / local FS (dev) | S3-compatible, presigned uploads off the app server; dev avoids paid services |
| Resend + React Email (prod) / console (dev) | Transactional email, React-rendered templates; dev needs no API key | |
| Charts | Recharts | Lightweight, composable, works in RSC |
| Validation | Zod | Shared between server actions and client forms |
| Testing | Vitest + Testing Library + Playwright | Fast unit/integration; E2E for critical paths |
| Export | exceljs / xlsx |
XLSX export & Excel PO import parsing |
| CI/CD | Forgejo + Forgejo Actions (self-hosted) | Issue→fix→PR pipeline; tag-triggered deploy |
| Hosting | Self-hosted on pms1 (Ubuntu), pm2 + native PostgreSQL |
Single-VM, full data control |
High-level system
┌─────────────────────────────────────────────┐
│ Browser — React 19 + shadcn/ui + Tailwind │
│ Server Components (read) + Client (forms) │
└───────────────────┬──────────────────────────┘
│ HTTPS
┌───────────────────▼──────────────────────────┐
│ Next.js 15 App Server │
│ App Router pages (RSC) │ Server Actions / │
│ │ Route Handlers │
│ ┌──────────────────────────────────────────┐│
│ │ Business logic: PO state machine, ││
│ │ permission checks, notifier ││
│ └──────────────────────────────────────────┘│
└───────┬───────────────┬───────────────┬───────┘
│ │ │
┌───────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
│ PostgreSQL │ │ Cloudflare │ │ Resend │
│ (Prisma) │ │ R2 (docs) │ │ (email) │
└──────────────┘ └─────────────┘ └─────────────┘
│
┌───────▼──────────────────────────────────────┐
│ GstService (Express + Playwright, :3003) │
│ GST portal CAPTCHA / taxpayer lookup proxy │
└───────────────────────────────────────────────┘
Key design decisions
- Server Components for all data-fetching pages; Client Components only where interactivity is needed.
- Server Actions for all mutations (create PO, approve, pay, etc.). There are no REST endpoints for mutations — route handlers exist only for auth, file signing/serving, GST proxying, notifications, search, and exports.
- Prisma
Decimalcannot cross into Client Components — convert withNumber()in the Server Component before passing as a prop (seepo-detail.tsx→lineItemsForEditor). - Storage and email toggle automatically on
NODE_ENV(R2/Resend in prod, local disk/console in dev). - One source of truth for state — every PO status change goes through
lib/po-state-machine.tsand is recorded as aPOAction(audit trail).
Application structure
app/
├── (auth)/login/
├── (portal)/ # authenticated shell
│ ├── layout.tsx # sidebar + header (role-aware)
│ ├── dashboard/
│ ├── my-orders/
│ ├── po/{new,import,[id],[id]/edit,[id]/receipt}/
│ ├── approvals/{,[id]}/
│ ├── payments/{,history}/
│ ├── history/
│ ├── inventory/{items,vendors,cart}/
│ └── admin/{users,companies,accounts,products,sites,vessels,vendors,superuser-requests}/
└── api/
├── auth/[...nextauth]/
├── files/{sign, dev/[...key]}/
├── gst/{, captcha}/
├── notifications/{, read}/
├── po/[id]/export/
├── po/import/
├── products/search/
└── reports/export/
lib/
├── db.ts # Prisma client singleton
├── auth helpers # (auth.ts at App root; NextAuth v5 config)
├── po-state-machine.ts # all valid status transitions + required roles
├── permissions.ts # role → allowed-action map
├── po-number.ts # structured PO number gen/parse
├── po-import-parser.ts # Excel PO parsing
├── notifier.ts # email dispatch (Resend prod / console dev)
├── storage.ts # file upload/download (R2 prod / local dev)
├── upload-files.ts # client-side upload helper
├── attachments.ts # PO document grouping
├── cart.ts # localStorage cart helpers
├── cost-centre-groups.ts # vessel grouping for selects
├── geo.ts # pincode → lat/long, distance
├── id-generators.ts # vendor/product code generation
├── feature-flags.ts # INVENTORY_ENABLED
├── forgejo.ts # Report-Issue → Forgejo API
├── utils.ts # formatCurrency, formatDate, status maps
└── validations/{po.ts,user.ts} # Zod schemas
See the Data Model, PO Lifecycle, and Roles and Permissions for the core domain logic.
API surface
All data mutations are Server Actions co-located with their page
(app/(portal)/*/actions.ts). Route handlers are reserved for:
| Route Handler | Method | Purpose |
|---|---|---|
/api/auth/[...nextauth] |
GET/POST | Auth.js session endpoints |
/api/files/sign |
POST | Generate R2 presigned upload URL (prod) |
/api/files/dev/[...key] |
GET/PUT | Local upload/download (dev only; 404 in prod) |
/api/gst |
POST | Proxy GST taxpayer search via GstService |
/api/gst/captcha |
GET | Proxy GST portal CAPTCHA image/session |
/api/notifications |
GET | Fetch the current user's notifications |
/api/notifications/read |
POST | Mark notifications read |
/api/po/[id]/export |
GET | Export single PO as PDF/XLSX (gated to MGR_APPROVED+) |
/api/po/import |
POST | Parse an uploaded Excel PO (Manager/SuperUser/Admin) |
/api/products/search |
GET | Fuzzy product search for the line-item editor |
/api/reports/export |
GET | Export PO history as CSV/PDF |
Auth & authorisation
- NextAuth v5 with a Microsoft Entra SSO provider and a credentials provider. Passwords hashed with bcrypt.
- SSO-only users have no
passwordHash(nullable); the profile page lets them optionally set one and is reachable by every role. - Authorisation is centralised in
lib/permissions.ts(hasPermission/requirePermission). Server Actions callrequirePermission()at the top before any DB write; Server Components gate whole page segments. Full matrix on Roles and Permissions.
Development conventions
- Trunk:
master. Work lands via PRs (feat//fix//chore/, orclaude/issue-Nfrom the automated pipeline). Production is whatevervX.Y.Ztag is deployed; staging is a deployed instance of latestmaster, not a branch. - Commits: Conventional Commits (
feat:,fix:,refactor:). - Migrations: never edit
schema.prismawithout generating and committing a migration; migration files are reviewed in PRs. - PR policy: every change goes through a PR; PRs add docs + tests.
- Secrets: never committed — server
~/pms/App/.env, local.env.local.
Pelagia Portal (PPMS)
Overview
Build & Run
System
Product
- Feature Catalogue
- Pages and Navigation
- Workflows
- Purchase Orders
- Vendors and GST Lookup
- Inventory and Catalogue
- Inventory on Approval
- Notifications
- File Storage
- Design System
Planned
Quality
Ops
Engineering
Pelagia Portal (PPMS) — internal purchase-order management. Self-hosted on pms1, live at pms.pelagiamarine.com. This wiki tracks the shipped product; authoritative sources are the repo code, App/CLAUDE.md, Docs/, and CHANGELOG.md.