See INSTALLING.md for full installation steps. The short version:
docker compose up -d db # Start PostgreSQL
cd backend && npm install && npm run dev # Backend on :3001
cd frontend && npm install && npm run dev # Frontend on :5173backend/
├── Dockerfile # node:20-slim + openssl, runs prisma db push + seed + server
├── package.json
├── tsconfig.json
├── prisma/
│ ├── schema.prisma # Single source of truth for the database schema
│ ├── seed.ts # Seed script (run via `npx tsx prisma/seed.ts`)
│ └── migrations/
│ ├── 0001_init/
│ │ └── migration.sql # Initial schema SQL
│ └── migration_lock.toml
└── src/
├── server.ts # Express app: CORS, JSON, cookie-parser, route mounting
├── middleware/
│ ├── auth.ts # authMiddleware (JWT verify) + adminOnly guard
│ └── errorHandler.ts # Global Express error handler
├── routes/
│ ├── auth.ts # POST login/refresh/logout, GET me
│ ├── clients.ts # CRUD — admin only
│ ├── engagements.ts # CRUD — admin only
│ ├── users.ts # CRUD + password change — admin only
│ ├── costRates.ts # CRUD — admin only
│ ├── assignments.ts # CRUD with overlap validation — admin only
│ ├── timesheets.ts # List + status transitions (submit, approve, send-back)
│ ├── timeEntries.ts # CRUD + hour validation + assignment lookup
│ ├── reports.ts # 3 report endpoints (JSON + Excel export) — admin only
│ └── margins.ts # Margin calculation — admin only
└── utils/
└── jwt.ts # Sign/verify helpers for access and refresh tokens
frontend/
├── Dockerfile # node:20-alpine, runs `npm run dev`
├── index.html # Single HTML entry point
├── package.json
├── tsconfig.json
├── vite.config.ts # Vite: React plugin, host 0.0.0.0
├── tailwind.config.ts # Tailwind content paths
├── postcss.config.js # PostCSS: Tailwind + Autoprefixer
└── src/
├── main.tsx # ReactDOM.createRoot
├── App.tsx # BrowserRouter + all route definitions
├── index.css # Tailwind directives (@tailwind base/components/utilities)
├── vite-env.d.ts # Vite client type reference
├── api/
│ └── client.ts # Axios instance + request/response interceptors
├── auth/
│ ├── AuthContext.tsx # React Context: user state, login, logout, silent refresh
│ ├── ProtectedRoute.tsx # Route guard (redirects to /login or /timesheets)
│ └── LoginPage.tsx # Login form
├── components/
│ ├── Layout.tsx # Header + sidebar + <Outlet />
│ ├── Modal.tsx # Generic modal dialog (esc to close, overlay click)
│ ├── MonthPicker.tsx # Left/right month navigation
│ ├── SearchableSelect.tsx# Dropdown with type-ahead filter
│ ├── Spinner.tsx # Loading spinner
│ └── ConfirmDialog.tsx # Delete confirmation modal
├── features/
│ ├── admin/
│ │ ├── ClientsPage.tsx
│ │ ├── EngagementsPage.tsx
│ │ ├── UsersPage.tsx
│ │ ├── CostRatesPage.tsx
│ │ ├── AssignmentsPage.tsx
│ │ └── AdminTimesheetsPage.tsx
│ ├── timesheets/
│ │ └── TimesheetPage.tsx
│ └── reports/
│ ├── UserMonthlyReport.tsx
│ ├── UserEngagementReport.tsx
│ ├── UserClientReport.tsx
│ └── MarginsPage.tsx
└── types/
└── index.ts # TypeScript interfaces (User, Client, Engagement, etc.)
- Language: TypeScript everywhere (strict mode).
- Backend framework: Express 4 with route-level middleware. Each route file exports a single Router.
- ORM: Prisma Client. Each route file creates its own
PrismaClient()instance. - Frontend state: React hooks + Context API (no external state management).
- Styling: Tailwind CSS utility classes. No component library.
- Notifications:
react-hot-toastfor success/error/warning toasts. - Dates: stored as
DATEin PostgreSQL, serialised as ISO strings in the API, formatted asDD/MM/YYYYin the UI. - Currency: formatted with 2 decimal places and the
€symbol in the UI.
-
Edit
backend/prisma/schema.prisma. -
Push changes to the local database:
cd backend DATABASE_URL="postgresql://marginio:marginio@localhost:5432/marginio" npx prisma db push
-
Regenerate the Prisma client:
npx prisma generate
-
If you need a migration file for deployment, generate the SQL diff:
npx prisma migrate diff \ --from-schema-datamodel prisma/schema.prisma \ --to-schema-datasource prisma/schema.prisma \ --script > prisma/migrations/XXXX_description/migration.sql
DATABASE_URL="postgresql://marginio:marginio@localhost:5432/marginio" npx tsx prisma/seed.tsThe seed script is idempotent — it deletes all existing data before inserting.
Both projects use TypeScript strict mode. To verify without emitting output:
# Backend
cd backend && npx tsc --noEmit
# Frontend
cd frontend && npx tsc --noEmitcd backend
npm run build # Compiles to dist/
npm start # Runs dist/server.jscd frontend
npm run build # Type-checks + produces dist/
npm run preview # Preview the build locallyThe dist/ folder contains static HTML/CSS/JS that can be served by any HTTP server or CDN.
| Variable | Where | Value |
|---|---|---|
DATABASE_URL |
Backend shell | postgresql://marginio:marginio@localhost:5432/marginio |
JWT_SECRET |
Backend shell | change-me-in-production |
JWT_REFRESH_SECRET |
Backend shell | change-me-refresh |
PORT |
Backend shell | 3001 |
VITE_API_URL |
Frontend shell | http://localhost:3001/api (default, no need to set) |