🌍 Social Impact Backend

YPF Backend Developer Guide

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.

⚑ Quick Overview

πŸ—„οΈ

Database Schemas

PostgreSQL with Drizzle ORM. Organized into 4 schemas: core, activities, finance, and shop.

πŸ”

Authorization

Flexible Visitor pattern with profile-based and role-based access control. Dynamic guards using anyOf() and allOf().

πŸ“

File Uploads

Media (images/videos) and documents uploaded via Multer, stored externally with ImageKit. Supports up to 250MB for media.

⏰

Time-Bound Profiles

Members, Admins, Volunteers have startedAt/endedAt for historical tracking and automatic status transitions.

🧩

Middleware Stack

Stacked in route files for visibility. Use authenticate (strict) or authenticateLax (optional auth).

πŸ”Œ

Real-time

Socket.IO for notifications and chat. Auth flows through the same cookie-based JWT tokens.

πŸ—οΈ Architecture Overview

core

Constituents, Members, Chapters, Committees, Admins

activities

Projects, Events, Announcements

finance

Donations, Dues, Transactions, Partnerships

shop

Products, Orders, Cart Items
πŸ’‘ Key Convention Use the @/ alias for imports (e.g., import dbClient from "@/configs/db"). Never use process.env directlyβ€”import variables from @/configs/env.

⏳ Time-Bound Profiles

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
βœ… Best Practice When querying active profiles, always filter by date ranges. The usersService.getConstituentProfiles() function demonstrates this pattern correctly.

πŸ” Authorization System

The authorization system uses a flexible Visitor pattern with composable guards. Guards check profiles (ADMIN, MEMBER) or specific roles (SUPER_ADMIN, chapter lead).

typescript configs/authorizer/index.ts
// 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

Usage in Routes

typescript features/api/v1/chapters/index.ts
// 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
);

Creating New Roles

typescript configs/authorizer/roles.ts
// 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))
  )
)
⚠️ Important Roles are stored as strings in the JWT after login (e.g., "MEMBER.chapterlead.{uuid}"). They're computed at login from admin_roles_assignments and member_titles_assignments tables.

πŸ“ File Upload

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
typescript Example: Uploading project media
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
  }
);

🧩 Middleware Patterns

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.

authenticate

Strict 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.

βœ… Correct Order authenticate β†’ validateParams β†’ validateQuery β†’ authorize β†’ validateBody β†’ handler

πŸ“ API Conventions

Response Format

typescript Consistent response structure
// 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 Architecture

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

πŸ”— Quick Links