Building Master Construction & Developers — Full-Stack CMS
A marketing website and custom admin CMS for a construction and architecture firm: projects, services, blog, gallery, consultation bookings and inquiries, with secure admin sign-in and media on Cloudflare R2, built as one Next.js app on Neon PostgreSQL.
- Next.js
- React
- TypeScript
- PostgreSQL
- Neon
- Tailwind CSS
- JWT
- Cloudflare R2
- Recharts
Private project, demo on request
This is client or employer work, so the source code is confidential and the live app sits behind a login. I can't share screenshots or credentials publicly, but I'm happy to walk you through it on a call: a live demo, the architecture and selected code.
Architecture
Overview
A marketing website and content management system for a construction and architecture firm, built as a single Next.js 15 App Router application. Visitors browse projects, services, a blog and a gallery, book consultations and send inquiries. The firm's staff manage all of that content, plus incoming leads, from a password-protected /admin dashboard. React client pages talk to Next.js Route Handlers that act as the backend. Content lives in Neon serverless PostgreSQL, and uploaded media goes to a Cloudflare R2 bucket.
System
Public site (React 19) Admin CMS (/admin)
\ /
\ DataContext + api.ts / middleware: no cookie -> /admin/login
v v
Next.js Route Handlers (/api/*)
projects | services | posts | gallery | web-info
bookings | inquiries | auth | upload | health
| | |
getAuthenticatedUser pg Pool S3 client
(JWT in httpOnly | |
cookie / Bearer) v v
Neon PostgreSQL Cloudflare R2
(site_* tables) <- /api/upload/media/[...key]Key design decisions
One deployable instead of two. The project started as a separate Express backend with its own frontend. I moved the backend into Next.js Route Handlers, so the API, the admin panel and the public site now share types, auth helpers and a single deploy.
Cookie-based sessions with checks in two places. Login compares bcrypt hashes and returns a signed JWT in an httpOnly, SameSite=Lax cookie, so client JavaScript never handles the token. Middleware sends anyone without a cookie away from /admin, and every write route verifies the token again on the server.
Media proxied through the app. Uploads go to R2 under foldered, timestamped keys. Pages reference /api/upload/media/... rather than bucket URLs, and the proxy serves files with immutable cache headers, so storage can move without rewriting stored content.
Singleton settings as JSON documents. Company info, hero and about settings are each stored as one JSON row and saved with INSERT ... ON CONFLICT DO UPDATE. Editors can add or change fields without a schema migration, while projects, posts, bookings and inquiries use normal columns.
The site keeps working if the API fails. A shared DataContext fetches every collection in parallel with Promise.allSettled and caches public content in localStorage. It falls back to bundled default content when a request fails, so one failed endpoint doesn't leave a page empty.
Stack
- Frontend: Next.js 15 (App Router, Turbopack), React 19, TypeScript, Tailwind CSS, Radix/shadcn UI, Framer Motion
- Backend: Next.js Route Handlers, jsonwebtoken, bcryptjs
- Data: Neon PostgreSQL via
pgconnection pool - Media: Cloudflare R2 through the AWS SDK S3 client
- Admin charts: Recharts
Key Implementation Highlights
Moved a standalone Express API into Next.js Route Handlers, putting the public site, admin CMS and API in one TypeScript codebase.
ArchitectureBuilt a CMS for projects, services, blog posts, gallery, site-wide settings, consultation bookings and contact inquiries.
AdminBuilt admin auth with bcrypt password checks, JWTs in httpOnly cookies, route middleware and token checks on every write endpoint.
SecurityBuilt drag-and-drop image uploads to Cloudflare R2, served back through a caching proxy route instead of public bucket URLs.
MediaDesigned PostgreSQL tables for content and leads, keeping singleton site settings as JSON documents saved with upserts.
DataAdded booking and inquiry inboxes with status tracking, filtering and CSV export of bookings.
AdminCached public content in localStorage and loaded collections in parallel, so repeat visits render before the network responds.
PerformanceShared one bounded pg connection pool through a global cache so development hot reloads stopped opening new Neon connections.
Backend
Interested in the technical details?
Let's Talk