Young Professionals Forum (YPF) is a social impact organization that solves societal problems through public projects. This guide covers key architectural patterns, conventions, and how-tos.
PostgreSQL with Drizzle ORM. Organized into 4 schemas:
core,
activities,
finance, and
shop.
Flexible Visitor pattern with profile-based and role-based access
control. Dynamic guards using
anyOf() and
allOf().
Media (images/videos) and documents uploaded via Multer, stored externally with ImageKit. Supports up to 250MB for media.
Members, Admins, Volunteers have
startedAt/endedAt
for historical tracking and automatic status transitions.
Stacked in route files for visibility. Use
authenticate (strict) or
authenticateLax (optional auth).
Socket.IO for notifications and chat. Auth flows through the same cookie-based JWT tokens.
@/ alias for imports (e.g.,
import dbClient from "@/configs/db").
Never use process.env directlyβimport
variables from
@/configs/env.
Entity profiles (Member, Admin, Volunteer, Auditor, Director) are
time-bound with startedAt and
endedAt columns. A profile is
"active" when
startedAt β€ now() AND (endedAt IS NULL OR endedAt β₯ now()).
| Table | Purpose | Key Fields |
|---|---|---|
core.members |
Membership periods for constituents | constituentId, startedAt, endedAt |
core.admins |
Admin appointment periods | constituentId, startedAt, endedAt |
core.admin_roles_assignments
|
Role assignments (SUPER_ADMIN, REGULAR_ADMIN) | adminId, role, startedAt, endedAt |
core.member_titles_assignments
|
Leadership titles (President, Chapter Lead, etc.) | memberId, titleId, startedAt, endedAt |
usersService.getConstituentProfiles()
function demonstrates this pattern correctly.
The authorization system uses a flexible Visitor pattern with composable guards. Guards check profiles (ADMIN, MEMBER) or specific roles (SUPER_ADMIN, chapter lead).
// Guard types available:
Visitors.ALL // Public access
Visitors.AUTHENTICATED // Any logged-in user
Visitors.hasProfile("ADMIN", "MEMBER") // Has one of these profiles
Visitors.hasRole(ADMIN.SUPER) // Has specific role
Visitors.hasID((req) => req.Params.id) // Matches constituent ID
// Combine guards:
anyOf(guard1, guard2) // OR: passes if ANY guard passes
allOf(guard1, guard2) // AND: passes if ALL guards pass
// Example: Allow ADMIN or resource owner
router.get(
"/constituents/:constituentId",
authenticate,
validateParams(z.object({ constituentId: z.string() })),
authorize(
anyOf(
Visitors.hasProfile("ADMIN"),
Visitors.hasID((req) => req.Params.constituentId)
)
),
handler
);
// 1. Define a new role matcher
export const MEMBER = {
// Matches any chapter lead role
CHAPTERLEAD: Role.matches(/^MEMBER\.lead\..+$/),
// Factory for specific chapter
chapterLead: (chapterId: string) =>
Role.new(`MEMBER.chapterlead.${chapterId}`),
};
// 2. Use in route authorization
authorize(
anyOf(
Visitors.hasRole(ADMIN.SUPER),
Visitors.hasRole((req) => MEMBER.chapterLead(req.Params.id))
)
)
"MEMBER.chapterlead.{uuid}"). They're
computed at login from
admin_roles_assignments and
member_titles_assignments tables.
Two upload types: Media (images/videos, up to 250MB) and Documents (PDFs/docs, up to 10MB). Files are stored temporarily via Multer, then uploaded to external storage.
| Type | Middleware | Allowed Types | Max Size |
|---|---|---|---|
| Media |
filesUpload.mediaUpload
|
PNG, JPEG, MP4, MOV, AVI | 250 MB |
| Documents |
filesUpload.documentsUpload
|
PDF, DOC(X), XLS(X), PPT(X), Images | 10 MB |
router.post(
"/:id/media",
authenticate,
filesUpload.mediaUpload.single("file"),
validateParams(...),
authorize(...),
async (req, res, next) => {
const file = req.file; // Multer file
const uploadMeta = await mediaUtils.storeMediumFile(file);
// uploadMeta contains: externalId, type, width, height, size
}
);
Middlewares are stacked directly in route files for visibility. This ensures you can always see what auth/validation is applied by reading the route definition.
authenticateStrict authentication. Returns 401 if not logged in. Use for protected endpoints.
authenticateLax
Optional authentication. Sets
req.User if token exists,
otherwise continues without error. Use for public endpoints with
optional personalization.
validateBody(schema)
Validates request body with Zod schema. Result accessible via
req.Body.
validateQuery(schema)
Validates query parameters. Result accessible via
req.Query.
validateParams(schema)
Validates URL parameters. Result accessible via
req.Params.
authorize(guard)Checks if user has permission. Returns 403 if guard fails. Must come after authentication.
authenticate β
validateParams β
validateQuery β
authorize β
validateBody β handler
// Success response
{
"success": true,
"data": { ... },
"message"?: "Optional message"
}
// Error response
{
"success": false,
"message": "Error description"
}
// Paginated response
{
"success": true,
"data": {
"items": [...],
"page": 1,
"pageSize": 10,
"total": 42
}
}
| Layer | Location | Responsibility |
|---|---|---|
| Routes |
features/api/v1/{feature}/index.ts
|
URL mapping, middleware orchestration, Swagger docs |
| Handlers |
features/api/v1/{feature}/{feature}Handler.ts
|
Thin layer: call services, wrap responses in ApiResponse |
| Services |
shared/services/{feature}Service.ts
|
Business logic, database queries, error throwing |
| Schemas |
features/api/v1/{feature}/schemas.ts
|
Zod validation schemas for requests |
| DTOs |
features/api/v1/{feature}/dtos.ts
|
Response type definitions |