ProductionCipta Craft · Full-Stack Dev · 2024

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.

100%Content managed by non-devs
ISRPage rebuilds in < 1s
Append-only audit trail

Tech Stack

Next.jsTypeScriptPostgreSQLJWTshadcn/uiDockerNginx

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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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
bash — setup
$git clone https://github.com/zafransakowi/anjung-meriah && cd anjung-meriah
$cp .env.example .env

# Set DATABASE_URL, JWT_SECRET, R2_BUCKET, REVALIDATE_TOKEN

$docker compose up -d postgres

# Starts local Postgres

$pnpm install && pnpm db:migrate

# Runs Drizzle ORM migrations

$pnpm dev:cms

# CMS on localhost:3001

$pnpm dev:public

# Public site on localhost:3000

Challenges

  1. 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.

  2. 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?'