case study

AMSY ERP

Architected and built an ERP for a group of four companies, gathering requirements directly from the client and from employees across different departments.

client:
AMSY Group
role:
Freelance Software Engineer
period:
Aug 2025 – present
status:
deployed
  • Java
  • Spring Boot
  • Spring Security
  • PostgreSQL
  • pg_dump
  • Next.js
  • TypeScript
  • WebSockets
  • Claude API
  • GCP
  • Google Secret Manager
  • Cloudflare R2
  • NGINX

Screenshots

Local development data, with every project name and figure blurred. Select a screenshot to zoom in.

  • PMO portfolio dashboard
  • Earned value management
  • Risk register and heat map
  • Project phases timeline
  • Home command center
  • One-page executive report
  • Bill of quantities
  • AI import review
  • Arabic, right to left
  • Fillable Word templates
  • Real-time chat, dark theme

What I Built

  1. Project Management

    tender and project management with bills of quantities, milestones, phases, cost and material tracking, and risk management. Project progress is measured with earned value management (EVM).

  2. AI Imports

    the Claude API turns messy legacy PDF and Excel files into structured BOQs, tenders and complete projects and fills in missing required data, with users reviewing everything before it’s imported.

  3. HR & Approvals

    employees, departments, documents, geofenced attendance, and configurable multi-step approval cycles for leave, loans, trips, financial and custom requests.

  4. Real-Time

    one-to-one and group chats with typing indicators, presence awareness (online and last active) and file attachments, plus live notifications, all over WebSockets.

  5. Object Storage

    private Cloudflare R2 bucket with short-lived presigned uploads and downloads, batched so many files need a single request. File types and sizes are enforced in the signed URL and verified after upload, with per-record access checks and scheduled cleanup of abandoned, orphaned and discarded files.

  6. Security

    granular permission-based access control, where roles bundle permissions and administrators assign roles or individual permissions to each user, plus endpoint-specific rate limiting.

  7. Deployment

    Google Compute Engine behind Cloudflare and NGINX, SSH only via IAP tunnels, secrets in Google Secret Manager, and observability with Google Cloud Ops Agent (metrics and logs). Deploys back up the database first and roll back automatically if the health check fails. Nightly public-key-encrypted pg_dump backups go to Cloudflare R2 with a 30-day immutability lock.

Architecture

How a request reaches the app, how administrators get in, and how files, deploys and backups work. This is the real setup, drawn without the production identifiers.

request path

One origin behind Cloudflare. NGINX sends pages to Next.js and /api and /ws to Spring Boot; only the backend can reach PostgreSQL.
  1. The browser sends every request (pages, /api and /ws) to one origin.
  2. Cloudflare proxies the DNS and caches static /_next/static assets; /api/* and /ws bypass the cache. TLS is Full (strict), with a Cloudflare origin certificate on NGINX, and WebSockets are enabled.
  3. The Google Cloud firewall only accepts port 443 from Cloudflare's IP ranges and port 22 from Google IAP's range. Port 80 is closed and there is no public database port, so direct traffic from the internet is dropped.
  4. On the VM, NGINX routes / to the Next.js front end and /api/ and /ws to the Spring Boot backend (REST and STOMP over WebSocket). Both apps listen on localhost only.
  5. PostgreSQL listens on localhost only. Only the Spring Boot backend connects to it; the Next.js front end has no connection to the database.
  6. Outbound from the VM: the backend calls the Claude API for AI imports, secrets are kept in Google Secret Manager, and the Ops Agent ships logs and metrics.
  7. Files go directly between the browser and a private Cloudflare R2 bucket, using short-lived presigned URLs that the backend issues.

admin access

No public SSH. Admins reach the VM, and the database, only through an Identity-Aware Proxy tunnel.
  1. From the admin's laptop: gcloud compute ssh with --tunnel-through-iap.
  2. Google's Identity-Aware Proxy checks the Google identity and IAM permissions before the tunnel opens.
  3. The VPC firewall accepts port 22 only from IAP's IP range, so there is no public SSH.
  4. The VM signs the admin in with OS Login. Database administration runs on the VM itself (psql against localhost), or from a desktop database client through a local port forward in the same IAP tunnel.
  5. From the open internet, port 22 (SSH), port 5432 (PostgreSQL) and port 80 (HTTP) are all blocked.

file uploads

Uploads skip the backend: it checks access and signs short-lived, type- and size-limited URLs; the browser sends files straight to a private R2 bucket.
  1. The browser asks the backend for upload URLs for a whole batch of files, in one request.
  2. The backend checks per-record access, then signs short-lived URLs that enforce the allowed file type and size, and returns them.
  3. The browser uploads each file directly to the private Cloudflare R2 bucket with a PUT; the file bytes do not go through the backend.
  4. After the upload, the backend verifies each file's type and size.
  5. A scheduled cleanup removes abandoned, orphaned and discarded files.
  6. Downloads work the same way: short-lived presigned URLs, issued after the same per-record access check.

deploys and backups

Backend deploy: refuse dirty trees, back up the database first, then switch releases with a 120 s health check and automatic rollback.
  1. Refuse to deploy if there are uncommitted changes.
  2. Build and run the tests.
  3. Upload a dated release. The 3 newest releases are kept.
  4. Back up the database before deploying; if the backup fails, the deploy aborts.
  5. Switch the release symlink to the new release and restart the service.
  6. Poll the health endpoint for up to 120 seconds.
  7. If the backend reports UP, it is live and the front end deploys next, with the same symlink switch, health check and rollback.
  8. If it is not UP within 120 seconds, the deploy rolls back automatically to the previous release.
Nightly pg_dump, encrypted with a public key and stored in an R2 bucket locked for 30 days. Only the private key, kept off the server, can decrypt it.
  1. Every night a scheduled job starts the backup.
  2. pg_dump dumps the PostgreSQL database.
  3. The dump is encrypted with a public key (age); the VM only holds the public key.
  4. The encrypted file goes to a Cloudflare R2 backup bucket with a 30-day bucket lock, so it can't be changed or deleted during that time.
  5. Only the holder of the private key, which is kept off the server, can decrypt a backup.
  6. Separately, the VM's boot disk is snapshotted daily and the snapshots are kept for 14 days.