Personal blog with admin CMS, built with Next.js 16, Drizzle ORM, and PostgreSQL. Supports Markdown writing, image upload via presigned URL, and deployment to AWS Lambda via Docker.
- Blog & Articles — article list, detail pages with Markdown rendering (GFM support)
- CMS Admin — CRUD operations (create, edit, delete) with session authentication
- Markdown Editor — GitHub Flavored Markdown support (tables, code blocks, etc.)
- Media Upload — image upload via presigned URL to S3
- Server Actions — form handling without separate API routes
- Responsive Design — optimal display on desktop and mobile
- Dark/Light Mode — theme toggle via shadcn/ui
- Node.js v22 or higher
- npm v10 or higher
- Docker (for deployment)
- Terraform v1.0+ (for AWS infrastructure)
- AWS CLI (for AWS deployment)
- PostgreSQL database (Neon, RDS, or local)
git clone https://github.com/username/aplikasi-catatan.git
cd aplikasi-catatannpm installCreate .env.local file:
DATABASE_URL='postgresql://username:password@host:5432/dbname?sslmode=require'
ADMIN_PASSWORD='your-secure-admin-password'# Push schema to database
npm run db:pushnpm run devnpm run dev # Run dev server
npm run lint # Check code with ESLint
npm run build # Build for productionnpm run db:push # Push schema to database
npm run db:generate # Generate migration files
npm run db:migrate # Run migrations
npm run db:studio # Open Drizzle Studio (GUI)# Build image
docker build -t aplikasi-catatan .
# Run container
docker run -p 3000:3000 --env-file .env.local aplikasi-catatan
# Or with Docker Compose (app + PostgreSQL)
docker compose up --buildnpm run infra:init # Initialize Terraform
npm run infra:plan # Preview infrastructure
npm run infra:apply # Create AWS infrastructure
git push origin main # Trigger CI/CDSee DEPLOY.md for full deployment guide.
| Path | Function |
|---|---|
/ |
Homepage — list of all articles |
/blog/[id] |
Article detail (Markdown rendering) |
/login |
Admin login |
/admin |
CMS dashboard — article CRUD |
/api/health |
Health check endpoint |
| Category | Technology |
|---|---|
| Framework | Next.js 16 (App Router) |
| UI Library | React 19 |
| Language | TypeScript 5 |
| Styling | Tailwind CSS v4 |
| Component | shadcn/ui (base-nova) |
| ORM | Drizzle ORM |
| Database | PostgreSQL (Neon / AWS RDS) |
| Deployment | AWS Lambda, CloudFront, S3 |
| CI/CD | GitHub Actions |
| Infra as Code | Terraform |
| Container | Docker |
| Command | Function |
|---|---|
npm run dev |
Run development server |
npm run build |
Build for production |
npm start |
Run production server |
npm run lint |
ESLint check |
npm run db:push |
Push schema to database |
npm run db:generate |
Generate migration files |
npm run db:migrate |
Run migrations |
npm run db:studio |
Open Drizzle Studio |
npm run infra:init |
Initialize Terraform |
npm run infra:plan |
Preview infrastructure |
npm run infra:apply |
Create AWS infrastructure |
npm run infra:destroy |
Destroy AWS infrastructure |
aplikasi-catatan/
├── src/
│ ├── app/ # Next.js App Router
│ │ ├── page.tsx # Homepage
│ │ ├── login/ # Login page
│ │ ├── admin/ # Admin dashboard
│ │ ├── blog/ # Article detail
│ │ └── api/ # API routes
│ ├── components/ # React components
│ ├── db/ # Database schema & connection
│ └── lib/ # Utilities
├── terraform/ # AWS infrastructure (IaC)
├── lambda/ # AWS Lambda handler
├── scripts/ # Migration scripts
├── public/ # Static assets
├── Dockerfile # Docker image (standalone)
├── Dockerfile.lambda # Docker image (AWS Lambda)
└── docker-compose.yml # Local development
┌──────────────────────────────────────────────────────────────────────────────┐
│ CI/CD Pipeline (GitHub Actions) │
│ [ Git Push ] → [ Build ] → [ Drizzle Migration ] → [ ECR ] │
│ ↓ │
│ [ Lambda Deploy ] │
│ ↓ │
│ [ CloudFront Invalidation ] │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ Runtime (AWS Cloud) │
│ │
│ [ Users ] → [ CloudFront CDN ] → [ API Gateway ] → [ Lambda (Next.js) ] │
│ ↓ │
│ [ RDS PostgreSQL ] │
│ │
│ [ Users ] → [ S3 Bucket ] ← [ Media Upload via Presigned URL ] │
└──────────────────────────────────────────────────────────────────────────────┘
| Component | Purpose |
|---|---|
| CloudFront | CDN for static assets and dynamic content caching |
| API Gateway (HTTP API) | REST API endpoint routing to Lambda |
| Lambda | Serverless Next.js application runtime |
| RDS PostgreSQL | Managed database (16.1, db.t3.micro) |
| S3 | Media storage for uploaded images |
| ECR | Docker image repository |
| VPC | Isolated network with public/private subnets |
| IAM Roles | Fine-grained permissions for each service |
- Read Request: User → CloudFront → API Gateway → Lambda → RDS
- Write Request: Admin → Lambda → RDS
- Media Upload: Admin → S3 (presigned URL) → Lambda stores reference in RDS
- Deployment: Git push → GitHub Actions → ECR → Lambda update → CloudFront invalidation
- HTTPS only via CloudFront and API Gateway
- Environment variables for sensitive data (DATABASE_URL, ADMIN_PASSWORD, AWS credentials)
- HMAC sessions for admin authentication
- IAM roles with least-privilege permissions
- VPC isolates Lambda and RDS from public internet
- S3 bucket policy restricts access to Lambda only
See DEPLOY.md for detailed deployment instructions.