This project demonstrates how Next.js App Router supports multiple rendering strategies β Static Site Generation (SSG), Server-Side Rendering (SSR), and Hybrid Rendering using Incremental Static Regeneration (ISR).
The goal is to understand when to use each strategy and how it impacts performance, scalability, and user experience in a real-world application.
Page: /about
Rendering Type: Static Site Generation (SSG)
Why this approach was chosen: The About page contains informational content that does not change frequently. Since it is the same for all users, it is ideal to generate this page at build time.
Caching Behavior:
- Page is generated once during build
- Served as static HTML for every request
- No server computation on page load
Performance Benefit: This results in the fastest possible load time and zero server cost per request, improving Time to First Byte (TTFB).
Page: /dashboard
Rendering Type: Server-Side Rendering (SSR)
Why this approach was chosen: The Dashboard displays user-specific and real-time data such as habit completion count and timestamps. This data must always be fresh and accurate.
Caching Behavior:
- Page is rendered on every request
- Caching is disabled using
cache: 'no-store' - Data is fetched at request time
Performance Trade-off: While SSR is slightly slower than static pages and increases server load, it guarantees real-time accuracy and personalization.
Page: /habits
Rendering Type: Incremental Static Regeneration (ISR)
Why this approach was chosen: The Habits page shows commonly used or popular habits that change occasionally but not in real time. It benefits from being fast while still staying up to date.
Caching & Revalidation Behavior:
- Page is generated at build time
- Revalidated every 60 seconds
- Next.js regenerates the page in the background
Performance Benefit: This provides near-static performance while keeping content reasonably fresh without rebuilding the entire app.
Screenshots and logs from the deployed application show:
- Static pages loading instantly without network re-fetch
- Dynamic pages making server requests on every refresh
- ISR pages updating only after the revalidation interval
(Attach screenshots of Network tab and console logs here)
If the application had 10x more users, using Server-Side Rendering for all pages would significantly increase server cost and response time.
To scale efficiently:
- Static pages would remain fully static
- Many SSR pages would be converted to ISR
- SSR would be limited only to pages requiring real-time or user-specific data
By using static caching and revalidation wisely, the application would handle higher traffic with lower cost and better performance.
Choosing the correct rendering strategy is a critical architectural decision. Static rendering improves speed and scalability, SSR ensures accuracy, and ISR provides the best balance for production applications.
This project is configured with three environments:
- Development
- Staging
- Production
Each environment has its own configuration using environment variables.
.env.developmentβ local development.env.stagingβ staging deployment.env.productionβ production deployment
Only .env.example is committed to GitHub to prevent exposing sensitive data.
All real secrets such as database credentials and API URLs are stored securely using GitHub Secrets. These values are injected during build or runtime and are never hardcoded in the application.
Separate build commands were used to verify each environment:
npm run build:stagingnpm run build:production
Each build points to the correct backend and configuration.
.envfiles are ignored via.gitignore- No secrets are committed to version control
- Frontend access is limited to
NEXT_PUBLIC_*variables only
Multi-environment setups reduce deployment risk by isolating development, testing, and production. This prevents accidental production failures and improves CI/CD reliability by catching issues earlier in staging environments.
This project demonstrates a modern cloud deployment workflow using Docker, CI/CD, and cloud platforms like AWS or Azure.
The application is containerized using Docker to ensure consistency across development and production environments.
GitHub Actions is used to automate the build process whenever code is pushed to the main branch. This ensures early detection of build failures and enforces consistent deployments.
In a production setup, the Docker image would be pushed to a cloud container registry and deployed using AWS ECS or Azure App Service.
- Consistent environments using Docker
- Automated builds with CI/CD
- Scalable and reliable cloud hosting
Using Docker and CI/CD reduces deployment errors and enables faster iteration. Cloud platforms further improve scalability and availability, making this approach suitable for production-grade applications.
Environment variables are managed using .env files located at the project root.
Only .env.example is committed to version control, while actual environment-specific files are ignored for security.
This approach follows industry best practices and ensures safe configuration across development, staging, and production environments.
The project uses TypeScript strict mode to catch potential errors at compile time, such as implicit any types, unused variables, and casing mismatches. This reduces runtime bugs and improves reliability.
ESLint enforces best practices and Next.js standards, while Prettier ensures consistent code formatting across the codebase. Conflicting rules are disabled to allow seamless integration.
Pre-commit hooks are configured using Husky and lint-staged to automatically run ESLint and Prettier on staged files. This guarantees that only clean, consistent code is committed to the repository.
This setup improves maintainability, enforces team-wide coding standards, and supports scalable development.
This project uses environment variables to manage configuration securely.
.env.local: Stores real credentials and is ignored by Git..env.example: Template file listing all required variables with placeholder values.
DATABASE_URL- Used for database connections
- Available only on the server
- Never exposed to the client
NEXT_PUBLIC_API_BASE_URL- Safe to expose to the frontend
- Used for API requests
NEXT_PUBLIC_APP_ENV- Indicates the current environment
- Only variables prefixed with
NEXT_PUBLIC_are accessible in client-side code. .env.localis ignored using.gitignoreto prevent accidental commits.- Sensitive variables are never accessed inside client components.
To replicate the setup:
- Copy
.env.exampleto.env.local - Replace placeholder values with real credentials
We follow a consistent branch naming strategy:
feature/<feature-name>fix/<bug-name>chore/<task-name>docs/<update-name>
This improves clarity, traceability, and collaboration.
All PRs use a standardized template to clearly communicate intent, changes, and validation steps.
Every PR is reviewed using a checklist covering:
- Code quality
- Functionality
- Linting and formatting
- Security and secret management
The main branch is protected with:
- Mandatory pull request reviews
- Required status checks
- Disabled direct pushes
This workflow ensures consistent code quality, prevents accidental breaking changes, and enables faster, safer collaboration within the team.
The Dockerfile containerizes the Next.js application by installing dependencies, building the app, and running it in production mode.
- app: Next.js frontend application
- db: PostgreSQL database with persistent volume
- redis: Redis cache for fast in-memory access
All services run in a shared Docker bridge network (localnet), enabling container-to-container communication using service names.
PostgreSQL data is persisted using a named Docker volume to prevent data loss on container restarts.
Service configuration is injected via environment variables, ensuring no secrets are hardcoded.
All containers were successfully built and run using Docker Compose, and the application was accessible at http://localhost:3000.
During setup, common issues such as port conflicts and dependency installation delays were resolved by adjusting service ports and using lightweight Alpine images.
- User: Represents a registered user
- Habit: Represents a habit created by a user
- HabitLog: Tracks daily habit completion
- One User β Many Habits
- One Habit β Many HabitLogs
- Unique email per user
- Unique habit log per day
- Cascading deletes maintain data consistency
The schema follows 1NF, 2NF, and 3NF to eliminate redundancy and ensure data integrity.
This design supports:
- Fast lookups via indexed columns
- Easy extension (streaks, reminders, analytics)
- Efficient querying for dashboards and reports
Database migrations were applied successfully and verified using Prisma Studio with seeded test data.
Prisma ORM provides a type-safe and developer-friendly way to interact with the PostgreSQL database. It auto-generates a client based on the schema, reducing runtime errors and improving productivity.
- Installed Prisma and initialized the
/prismafolder - Defined normalized data models for User, Habit, and HabitLog
- Generated Prisma Client using
npx prisma generate - Integrated Prisma into the Next.js app using a singleton client
import { PrismaClient } from "@prisma/client";