Anjung Meriah CMS
Full-stack corporate platform with a public-facing marketing website and a secure internal CMS. Designed for zero developer intervention in daily operations — all content managed through the admin panel.
Tech Stack
Stakeholders
Property Developer (Client)
Business owner — defines content categories, approves design system, and manages day-to-day content
Marketing Team
Primary CMS users — upload property listings, promotions, and media assets
Site Visitors
Public-facing audience — prospective property buyers browsing listings
Zafran (Developer)
Full system design, frontend, backend, CMS, deployment, and ongoing support
The Problem
A property development firm needed their website rebuilt, but had no technical team to manage content updates. Every text change required contacting the original developer and waiting days for deployment.
The Solution
Built a decoupled architecture: a statically generated Next.js public site (ISR for near-instant rebuilds) and a private admin CMS protected by JWT auth with rate limiting. All models are database-driven and fully CRUD-capable from the dashboard.
Architecture
Monorepo with two Next.js applications sharing a PostgreSQL database. The public site is statically generated with ISR — each page revalidates on a schedule or on-demand when the CMS triggers a revalidation webhook. The admin panel is a separate Next.js app behind JWT auth, served from the same Docker host via Nginx reverse proxy.
- 01
Public Site (Next.js ISR)
Statically generated pages for property listings, services, and promotions. On-demand revalidation triggered from the CMS on any content save. Cloudflare CDN sits in front for global edge caching.
- 02
Admin CMS (Next.js App Router)
Server Actions for all mutations. shadcn/ui component library for consistent admin UI. JWT sessions stored in httpOnly cookies. Rate limiting middleware on auth endpoints.
- 03
Database (PostgreSQL)
Single database shared by both apps. Separate schemas for public-readable content and admin audit logs. Append-only audit_log table records every mutation with actor, timestamp, and diff.
- 04
Media Pipeline
File uploads go directly to Cloudflare R2 from the browser via pre-signed URLs. Metadata (filename, size, alt text, URL) is stored in the DB. No server memory pressure from file uploads.
- 05
Infra (Docker + Nginx)
Both Next.js apps containerised with Docker. Nginx reverse proxy routes traffic by subdomain. Deployed on a self-hosted VPS. Let's Encrypt certificates auto-renewed via Certbot.
Dev Setup
Prerequisites
- Node.js 20+
- pnpm
- Docker + Docker Compose
- PostgreSQL 16
# Set DATABASE_URL, JWT_SECRET, R2_BUCKET, REVALIDATE_TOKEN
# Starts local Postgres
# Runs Drizzle ORM migrations
# CMS on localhost:3001
# Public site on localhost:3000
Challenges
- 01
On-demand ISR without a paid plan
Vercel's on-demand revalidation is straightforward on their platform but we were self-hosting. Implemented a custom revalidation webhook endpoint in the public site that the CMS calls after each save — secured with a shared secret token — which calls Next.js's res.revalidate() internally.
- 02
Media upload UX for non-technical users
The client's marketing team had no experience with file size constraints or image formats. Added client-side image compression (browser-image-compression library) before upload and enforced 5MB hard limits with clear error messages. Reduced average upload size by 70%.
What I Learned
- 01
ISR on self-hosted Next.js requires deliberate plumbing — the revalidation webhook pattern is simple once understood but not obvious from the docs.
- 02
Designing for non-technical users means UX constraints are as important as API design.
- 03
An append-only audit log is cheap to build upfront and invaluable when clients ask 'who changed this?'