Imported from VENKATNITHIN007/AOMI-editorial (
AGENTS.md). Install upstream withnpx skills add VENKATNITHIN007/AOMI-editorial. Copyright stays with the author.
PHOTOPHILE - PROJECT KNOWLEDGE BASE
Generated: 2026-03-31 Type: Full-Stack Photography Marketplace Stack: Next.js 15 (Frontend) + Express/TypeScript (Backend)
Overview
Photophile (codename: Dukan) connects photographers with clients. Photographers create profiles and showcase portfolios, while clients discover photographers and contact them directly.
Product Direction (Persisted Context)
Current MVP scope is profile-first and portfolio-first:
- No booking flow in MVP
- No review/rating flow in MVP
- Primary goal: great photographer profile pages and portfolio showcase
Roadmap & Master Plan
See docs/master-plan.md for the detailed refactoring and development roadmap.
MVP Goals
- Photographers can register/login
- Photographers can create/edit profile and manage portfolio
- Each photographer has a public shareable URL:
/photographers/[username] - Public profile should feel like a mini-website and act as the highlighted page
- Architecture Rule: Strict separation between Identity and Business:
/profile(Account Settings): Managed by both users and photographers for Identity (Name, Avatar, Phone)./photographer/dashboard(Studio Dashboard): Managed ONLY by photographers for Business (Portfolio, Pricing, Bio).
- Visitors/customers can browse photographers without authentication
- Visitor authentication is optional in MVP
- Main conversion is direct contact with photographers (email/phone/WhatsApp/social)
Priority
- Public photographer page quality and shareability
- Portfolio upload and presentation quality
- Direct contact CTAs and lead capture
- Discovery/browse experience
- Keep auth simple and focused on photographer management only
Agent Instruction
When proposing features or implementation plans, prioritize the MVP scope above and avoid introducing booking/review complexity unless explicitly requested.
Current Status (2026-04-07)
- Backend build:
npm run buildpasses (npx tsc -b) - Frontend build:
npm run buildpasses - Implemented surface area: backend exposes auth, users, photographers, portfolio, reviews, and bookings routes; current MVP focus is public profiles, portfolio presentation, and direct contact
Structure
├── backend/ # Express.js API │ └── src/ │ ├── app.ts # Entry point │ ├── config.ts # Environment config │ ├── constants/ # App constants (modular) │ │ ├── error.ts # Error messages │ │ ├── pagination.ts │ │ ├── upload.ts │ │ └── booking.ts │ ├── controllers/ # Route handlers │ ├── db/ # Database connection │ ├── middlewares/ # Express middleware │ ├── models/ # Mongoose schemas │ ├── routes/ # API routes │ ├── types/ # TypeScript types │ ├── utils/ # Utilities │ │ ├── helper/ # Helper functions (modular) │ │ │ ├── jwt.util.ts │ │ │ ├── password.util.ts │ │ │ ├── route.util.ts │ │ │ ├── string.util.ts │ │ │ └── pagination.ts │ │ ├── ApiError.ts │ │ ├── ApiResponse.ts │ │ ├── asyncHandler.ts │ │ └── cloudinary.ts │ └── validations/ # Zod schemas
└── frontend/ # Next.js 15 App └── src/ ├── app/ # App Router pages ├── components/ # React components │ ├── forms/ # Form components │ ├── gallery/ # Gallery components │ ├── layout/ # Layout components │ ├── photographer/ # Photographer components │ ├── search/ # Search components │ ├── filters/ # Filter components │ └── ui/ # shadcn/ui components ├── contexts/ # React contexts (auth) ├── hooks/ # Custom React hooks ├── lib/ # Utilities │ └── validations/ # Frontend Zod schemas ├── styles/ # CSS styles └── middleware.ts # Next.js middleware
---
## Where to Look
| Task | Location | Notes |
|------|----------|-------|
| API entry | `backend/src/app.ts` | Express setup, middleware, routes |
| Database | `backend/src/db/` | MongoDB connection |
| Auth (backend) | `backend/src/controllers/user.controller.ts` | JWT login/register |
| Auth middleware | `backend/src/middlewares/auth.middleware.ts` | JWT verification |
| Models | `backend/src/models/` | User, Photographer, Portfolio, Booking, Review |
| API routes | `backend/src/routes/` | Mounted in app.ts |
| Validations | `backend/src/validations/` | Zod schemas |
| Frontend entry | `frontend/src/app/` | Next.js pages |
| Auth context | `frontend/src/contexts/` | React auth provider |
| Middleware | `frontend/src/middleware.ts` | Route protection |
| API client | `frontend/src/lib/api-client.ts` | Axios with interceptors |
---
## Conventions
### Backend
**Controllers:** Export handler functions, use async/await
```typescript
// controllers/example.controller.ts
export const getItems = async (req: Request, res: Response) => {
// ... logic
res.json({ data });
};
Routes: Mount controllers in route files
// routes/example.routes.ts
import { Router } from "express";
import { getItems } from "../controllers/example.controller";
const router = Router();
router.get("/", getItems);
export default router;
Models: Mongoose with TypeScript
- Define interface extending
Document - Export model as default
- Use
{ timestamps: true }
Middleware Pattern:
// JWT auth middleware
// Attaches `user` to req object after verifying token
Frontend
- App Router: All pages in
app/directory - Client components: Mark with
"use client" - API calls: Use axios, base URL from env
- Auth: Context-based, stores tokens, redirects on 401
Error Handling
Backend Error Handling
ApiError Class: Custom error class for consistent API responses
// backend/src/utils/ApiError.ts
class ApiError extends Error {
public readonly success: boolean = false;
public readonly statusCode: number;
public readonly data: null = null;
public readonly errors: unknown;
public readonly stack?: string;
public message = "Something went wrong";
constructor(
statusCode: number = 500,
message: string = "Something went wrong",
errors: unknown = "",
stack = ""
) {
super(message);
this.success = false;
this.statusCode = statusCode;
this.message = message;
this.data = null;
this.errors = errors;
if (stack) {
this.stack = stack;
} else {
Error.captureStackTrace(this, this.constructor)
}
}
toJSON() {
return {
success: this.success,
statusCode: this.statusCode,
message: this.message,
data: this.data,
errors: this.errors,
stack: appConfig.debug ? this.stack : undefined,
}
}
}
asyncHandler Wrapper: Eliminates try-catch boilerplate
// backend/src/utils/asyncHandler.ts
export const asyncHandler = (fn: RequestHandler): RequestHandler => {
return (req: Request, res: Response, next: NextFunction) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
};
// Usage
export const loginUser = asyncHandler(async (req, res) => {
throw new ApiError(401, "Invalid credentials");
});
Centralized Error Handler: Handles all error types
// backend/src/middlewares/errorHandler.middleware.ts
export const errorHandler: ErrorRequestHandler = (err, req, res, next) => {
// Handle ApiError
if (err instanceof ApiError) {
return res.status(err.statusCode).json(err.toJSON());
}
// Handle Zod validation errors
if (err instanceof ZodError) {
const formattedErrors = err.errors.map((e) => ({
field: e.path.join("."),
message: e.message,
}));
return res.status(400).json(
new ApiError(400, "Validation failed", formattedErrors).toJSON()
);
}
// Handle Mongoose validation errors
if (err instanceof mongoose.Error.ValidationError) { ... }
// Handle MongoDB duplicate key errors
if (err.code === 11000) { ... }
// Fallback for unhandled errors
return res.status(500).json(new ApiError(500, message).toJSON());
};
Security
Backend Security
Security Headers Middleware:
// backend/src/middlewares/security.middleware.ts
export const securityHeaders = (req, res, next) => {
res.setHeader("X-Frame-Options", "DENY"); // Prevent clickjacking
res.setHeader("X-Content-Type-Options", "nosniff"); // Prevent MIME sniffing
res.setHeader("X-XSS-Protection", "1; mode=block"); // Enable XSS filter
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
res.setHeader("Content-Security-Policy", "default-src 'self'; ...");
if (process.env.NODE_ENV === "production") {
res.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
}
next();
};
Rate Limiting:
// backend/src/middlewares/rateLimiter.middleware.ts
export const rateLimiter = (windowMs = 60 * 1000, maxRequests = 100) => {
return (req, res, next) => {
// Track requests per IP
// Return 429 if limit exceeded
};
};
// Auth endpoints get stricter limits
export const authRateLimiter = rateLimiter(15 * 60 * 1000, 5); // 5 per 15 min
export const apiRateLimiter = rateLimiter(60 * 1000, 100); // 100 per minute
Cookie Configuration:
// backend/src/config.ts
export const clearCookieOptions = {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict' as const,
};
export const accessTokenCookieOptions = {
...clearCookieOptions,
maxAge: 24 * 60 * 60 * 1000, // 1 day
};
export const refreshTokenCookieOptions = {
...clearCookieOptions,
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
};
Frontend Security
Next.js Middleware for Auth:
// frontend/src/middleware.ts
export async function middleware(request: NextRequest) {
const accessToken = request.cookies.get('accessToken')?.value;
const isAuthenticated = await verifyToken(accessToken);
// Protect dashboard routes
if (isProtectedPath && !isAuthenticated) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('redirect', pathname);
return NextResponse.redirect(loginUrl);
}
// Redirect authenticated users away from auth pages
if (isAuthPath && isAuthenticated) {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
return NextResponse.next();
}
Validation
Backend Validation
Zod Schemas:
// backend/src/validations/auth.validation.ts
import { z } from 'zod';
export const LoginSchema = z.object({
email: z.string().email('Invalid email'),
password: z.string()
.min(8, 'Password must be at least 8 characters')
.refine((pwd) => /[A-Z]/.test(pwd), { message: "Need uppercase" })
.refine((pwd) => /[a-z]/.test(pwd), { message: "Need lowercase" })
.refine((pwd) => /\d/.test(pwd), { message: "Need number" })
.refine((pwd) => /[!@#$%^&*]/.test(pwd), { message: "Need special char" }),
});
export type loginType = z.infer<typeof LoginSchema>;
validateRequest Middleware:
// backend/src/middlewares/validateRequest.middleware.ts
export const validateRequest = (schema: ZodType) => {
return function (req: Request, res: Response, next: NextFunction) {
try {
schema.parse({
...req.body,
...req.files,
...req.file,
});
next();
} catch (error) {
next(error);
}
};
};
// Route usage
router.post("/login", validateRequest(LoginSchema), loginUser);
API Versioning
All API routes include a version prefix (/api/v1/):
// backend/src/utils/helper/route.util.ts
export const createVersionRoute = (route: string, version: number = 1) =>
"/api/v" + version + "/" + route;
// Usage in app.ts
app.use(createVersionRoute("users"), userRouter);
// Results in: /api/v1/users
Frontend Patterns
API Client with Interceptors
// frontend/src/lib/api-client.ts
import axios from 'axios';
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001/api/v1';
export const apiClient = axios.create({
baseURL: API_URL,
withCredentials: true, // Crucial for HTTP-only cookies
headers: {
'Content-Type': 'application/json',
},
});
// Token refresh on 401
apiClient.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
await axios.post(`${API_URL}/users/refresh-token`, {}, { withCredentials: true });
return apiClient(originalRequest);
} catch (err) {
return Promise.reject(err);
}
}
return Promise.reject(error);
}
);
shadcn/ui Usage
UI components are built with shadcn/ui (Radix UI primitives + Tailwind):
// Components located in frontend/src/components/ui/
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card } from "@/components/ui/card";
Components are copy-paste (not npm packages), giving full control over styling and behavior.
API Endpoints
Base: /api/v1
| Resource | Path | Description |
|---|---|---|
| Auth | /auth |
CSRF, register, login, refresh, email verification, password reset, logout |
| Users | /users |
Current user and profile update |
| Photographers | /photographers |
Profiles, search |
| Portfolio | /portfolio |
Uploads, CRUD |
| Bookings | /bookings |
Create, manage |
| Reviews | /reviews |
Ratings, comments |
Environment Variables
Backend (backend/.env)
PORT=3001
ORIGIN_HOSTS=http://localhost:3000
APP_DEBUG=false
# JWT
ACCESS_TOKEN_SECRET=random-string
ACCESS_TOKEN_EXPIRY=6h
REFRESH_TOKEN_SECRET=random-string
REFRESH_TOKEN_EXPIRY=10d
# Cloudinary (image uploads)
CLOUDINARY_CLOUD_NAME=
CLOUDINARY_API_KEY=
CLOUDINARY_API_SECRET=
# Database
MONGO_URL=mongodb://localhost:27017
DB_NAME=photophile
Frontend (frontend/.env.local)
NEXT_PUBLIC_API_URL=http://localhost:3001/api/v1
ACCESS_TOKEN_SECRET=random-string # For middleware JWT verification
Commands
# Backend
cd backend/
npm install
npm run dev # nodemon + ts-node on port 3001
npm run build # tsc compile
npm start # node dist/index.js
# Frontend
cd frontend/
npm install
npm run dev # Next.js dev on port 3000 (Turbopack)
npm run build # Next.js build
Testing
Current state:
- Backend: no automated test suite configured yet
- Frontend: Playwright is configured (
frontend/playwright.config.ts) with e2e specs infrontend/e2e/ frontend/package.jsonincludestestandtest:uiscripts
Notes
- Two separate servers - frontend (3000) + backend (3001)
- CORS: Backend allows requests from
ORIGIN_HOSTS - Auth: JWT access token (6h expiry) + refresh token (10d)
- Uploads: Multer + Cloudinary for images
- Database: MongoDB with Mongoose
- Status: Backend compiles successfully; frontend currently blocked by one TypeScript error on dashboard bookings rendering
- Documentation: See
docs/decisions.mdfor architectural decisions anddocs/patterns.mdfor detailed coding patterns