Skip to content

Marketing — Banners, Events, League & Course Interest

This document covers the marketing surfaces of the Mr. Mentor / MAS backend: the sticky and pop-up banners that ride on top of the public website, the reusable banner-template asset library, the key/value event configuration store, the inter-IIT "League" registration intake (with resume upload), and the lightweight lead-capture forms for course interest and the "AI for Everyone" program. These are mostly thin CRUD + public-read features whose job is to let the admin/marketing team push promotional content to visitors and collect inbound interest.

Status: documented from source on this branch.


Overview

The marketing domain is a loosely-coupled collection of small features that share a common shape — an admin-managed catalog plus a public read or public submit endpoint that the marketing website (mas-website-live) and the admin dashboard (mr-mentor-frontend) consume over HTTP.

Surface What it does Primary persona
Sticky banners The thin promotional bar pinned to the top of the public site. Admin curates a list, picks one "active" banner via a settings singleton, frontend renders it. Marketing admin (author) → website visitor (reader)
Pop-up banners Modal/overlay promos on the public site, same model as sticky banners but with an image and four built-in MAS-program templates. Marketing admin → website visitor
Banner templates A reusable image-asset library (uploaded to S3) tagged by category, used as design starting points. Marketing admin
Event configuration A generic key/value store for event-related settings (URLs, text, flags) the website reads at runtime — e.g. a registration link that marketing can change without a deploy. Marketing admin → website
League registration Public intake form for an inter-IIT league/event, including a resume file upload to S3. Admin reviews, exports, and downloads resumes. Student/visitor (submit) → admin (review)
Course interest Public "I'm interested" capture for courses/cohorts, with admin triage (status, notes, assignment). Visitor (submit) → sales/marketing (triage)
AI for Everyone interest A dedicated interest-capture form for the "AI for Everyone" program; same pattern as course interest. Visitor → sales/marketing

Place in the suite. These features are downstream of the public website and the admin dashboard. They do not participate in auth, payments, or the meeting/token core. The interest-capture forms are adjacent to the Sales CRM (assignedTo, status, lastContactedAt mirror lead-triage semantics) but — importantly — they do not currently create CRM Lead rows or fire any cross-repo events (see Edge cases and Related docs).


Key concepts & entities

Glossary

  • Active banner — A banner with isActive = true. The public endpoint, however, does not return all active banners; it resolves a single chosen banner through the settings singleton (see Status lifecycles).
  • Settings singleton — Each banner type has exactly one settings row (sticky_banner_settings / popup_banner_settings), loaded with findOne({ where: {} }) and auto-created with defaults on first read. It holds the master isEnabled toggle, mobile/desktop visibility, and the activeBannerId pointer.
  • Virtual / system-default banner — Hard-coded, in-memory banner objects with IDs like SYSTEM_DEFAULT_50 or SYSTEM_DEFAULT_FLAGSHIP. They are never persisted; the public resolver returns one when activeBannerId matches a magic string or when no DB banner is available.
  • isDefault banner — A persisted banner flagged as the fallback. Setting one default imperatively unsets all others. Used by the public resolver when activeBannerId resolves to nothing.
  • Event configuration key — A unique string key (e.g. a registration-URL key) whose value the website fetches in bulk as a { key: value } map.
  • Banner category — Enum (batch | course | event | general) used to organise template assets and their S3 folder path.
  • Interest status — Triage state for captured interest (new | contacted | converted | not_interested).

Main entities (file paths)

Entity Table File
StickyBanner, StickyBannerSettings, BannerDisplayMode (enum) sticky_banners, sticky_banner_settings src/entities/StickyBanner.ts
PopupBanner, PopupBannerSettings popup_banners, popup_banner_settings src/entities/PopupBanner.ts
BannerTemplate, BannerCategory (enum) banner_templates src/entities/BannerTemplate.ts
EventConfiguration, ConfigType (enum) event_configurations src/entities/EventConfiguration.ts
LeagueRegistration league_registrations src/entities/LeagueRegistration.ts
CourseInterest, InterestStatus, CourseCategory (enums) course_interests src/entities/CourseInterest.ts
AiForEveryoneInterest, InterestStatus (enum) ai_for_everyone_interest src/entities/AiForEveryoneInterest.ts

Architecture

Routes → controllers → services → entities/DB. Sticky and pop-up banners and event config are mounted at /api; course interest is mounted at /api/interest; banner templates live inside the admin router (/api/admin).

flowchart TD
    subgraph clients["Clients"]
        WEB["mas-website-live (public site)"]
        ADMIN["mr-mentor-frontend (admin dashboard)"]
    end

    subgraph routes["Route layer (src/routes)"]
        R1["stickyBanner.routes.ts (/api)"]
        R2["popupBanner.routes.ts (/api)"]
        R3["admin.routes.ts -> banner templates (/api/admin)"]
        R4["eventConfiguration.routes.ts (/api)"]
        R5["leagueRegistration.routes.ts (/api)"]
        R6["courseInterest.routes.ts (/api/interest)"]
        R7["aiForEveryone.routes.ts (/api)"]
    end

    subgraph controllers["Controllers (src/controllers)"]
        C1["StickyBannerController"]
        C2["PopupBannerController"]
        C3["BannerTemplateController"]
        C4["EventConfigurationController"]
        C5["LeagueRegistrationController"]
        C6["courseInterest.controller (functions)"]
        C7["AiForEveryoneController"]
    end

    subgraph services["Services (src/services)"]
        S1["StickyBannerService"]
        S2["PopupBannerService"]
        S3["BannerTemplateService"]
        S4["EventConfigurationService"]
        S5["LeagueRegistrationService"]
        S7["AiForEveryoneService"]
        S3S["S3Service"]
    end

    subgraph db["PostgreSQL (TypeORM)"]
        T1["sticky_banners + settings"]
        T2["popup_banners + settings"]
        T3["banner_templates"]
        T4["event_configurations"]
        T5["league_registrations"]
        T6["course_interests"]
        T7["ai_for_everyone_interest"]
    end

    S3B["AWS S3 buckets"]

    WEB --> R1 & R2 & R4 & R5 & R6 & R7
    ADMIN --> R1 & R2 & R3 & R4 & R5 & R7

    R1 --> C1 --> S1 --> T1
    R2 --> C2 --> S2 --> T2
    R3 --> C3 --> S3 --> T3
    C3 --> S3S
    R4 --> C4 --> S4 --> T4
    R5 --> C5 --> S5 --> T5
    C5 --> S3S
    R6 --> C6 --> T6
    R7 --> C7 --> S7 --> T7
    S3S --> S3B
    S3 --> S3S

Note: courseInterest.controller is a set of plain functions that talk to the repository directly via DatabaseService.getInstance() — it has no dedicated service class.


Data model

These tables are independent — there are no foreign-key relationships between any of them. assignedTo on the interest tables and activeBannerId on the settings tables are bare UUID/varchar columns, not TypeORM relations.

erDiagram
    STICKY_BANNERS {
        uuid id PK
        varchar title
        varchar subtitle
        json chips
        varchar buttonText
        varchar buttonLink
        varchar tagText
        varchar backgroundColor
        varchar borderColor
        varchar buttonColor
        varchar textColor
        varchar tagColor
        int displayOrder
        boolean isActive
        varchar designTemplate
        boolean isDefault
        timestamp createdAt
        timestamp updatedAt
    }
    STICKY_BANNER_SETTINGS {
        uuid id PK
        varchar activeBannerId
        enum displayMode
        int transitionDuration
        boolean isEnabled
        boolean showOnMobile
        boolean showOnDesktop
    }
    POPUP_BANNERS {
        uuid id PK
        varchar title
        varchar subtitle
        varchar buttonText
        varchar buttonLink
        varchar imageUrl
        varchar backgroundColor
        varchar textColor
        varchar buttonColor
        int displayOrder
        boolean isActive
        varchar designTemplate
        boolean isDefault
    }
    POPUP_BANNER_SETTINGS {
        uuid id PK
        varchar activeBannerId
        boolean isEnabled
        boolean showOnMobile
        boolean showOnDesktop
    }
    BANNER_TEMPLATES {
        uuid id PK
        varchar name
        text description
        enum category
        varchar imageUrl
        varchar s3Key
        int width
        int height
        boolean isActive
        simple_array tags
    }
    EVENT_CONFIGURATIONS {
        uuid id PK
        varchar key UK
        text value
        varchar label
        enum type
        varchar category
        boolean isActive
        varchar description
    }
    LEAGUE_REGISTRATIONS {
        uuid id PK
        varchar fullName
        varchar phone
        varchar email
        varchar college
        varchar yearOfPassing
        varchar department
        varchar domain
        jsonb profiles
        text resumeUrl
        text resumeKey
        varchar resumeFileName
    }
    COURSE_INTERESTS {
        uuid id PK
        varchar name
        varchar email
        varchar phone
        varchar interestedIn
        enum category
        enum status
        text notes
        varchar source
        uuid assignedTo
        timestamp lastContactedAt
    }
    AI_FOR_EVERYONE_INTEREST {
        uuid id PK
        varchar name
        varchar email
        varchar phone
        varchar college
        enum status
        text notes
        varchar source
        uuid assignedTo
        timestamp lastContactedAt
    }

    STICKY_BANNER_SETTINGS ||..o| STICKY_BANNERS : "activeBannerId (soft pointer)"
    POPUP_BANNER_SETTINGS ||..o| POPUP_BANNERS : "activeBannerId (soft pointer)"

Notable enums & status values

  • BannerDisplayMode (sticky settings): single | multiple | auto_scroll — default single.
  • BannerCategory (templates): batch | course | event | general — default general.
  • ConfigType (event config): url | text | json | boolean — default url.
  • InterestStatus (course interest + AI interest): new | contacted | converted | not_interested — default new.
  • CourseCategory (course interest): cohort | capsule | accelerator | deeptech | platform — no default (required on create).

The activeBannerId pointer is a soft reference (a varchar(36)), not an enforced FK. It can also hold the magic strings SYSTEM_DEFAULT_* that resolve to in-memory virtual banners rather than DB rows.


API surface

Paths below are the full external paths (route-file path + mount prefix from src/routes/index.ts). Banner-template routes additionally require adminMiddleware; all other admin endpoints in this domain are guarded only by authMiddleware (or, for interest forms, nothing — see gotchas).

Sticky banners — StickyBannerRoutes mounted at /api

Method Path Auth/role Purpose
GET /api/sticky-banners/public Public Resolve the single banner to show + settings (website read)
GET /api/admin/mas/sticky-banners authMiddleware List all banners
GET /api/admin/mas/sticky-banners/:id authMiddleware Get one banner
POST /api/admin/mas/sticky-banners authMiddleware Create banner
PUT /api/admin/mas/sticky-banners/:id authMiddleware Update banner
DELETE /api/admin/mas/sticky-banners/:id authMiddleware Delete banner
POST /api/admin/mas/sticky-banners/reorder authMiddleware Reorder (orderedIds[])
PATCH /api/admin/mas/sticky-banners/:id/toggle authMiddleware Toggle isActive
GET /api/admin/mas/sticky-banners-settings authMiddleware Read settings singleton
PUT /api/admin/mas/sticky-banners-settings authMiddleware Update settings singleton

Pop-up banners — PopupBannerRoutes mounted at /api

Method Path Auth/role Purpose
GET /api/popup-banners/public Public Resolve the single pop-up + settings (website read)
GET /api/admin/mas/popup-banners authMiddleware List all pop-ups
GET /api/admin/mas/popup-banners/:id authMiddleware Get one pop-up
POST /api/admin/mas/popup-banners authMiddleware Create pop-up
PUT /api/admin/mas/popup-banners/:id authMiddleware Update pop-up
DELETE /api/admin/mas/popup-banners/:id authMiddleware Delete pop-up
POST /api/admin/mas/popup-banners/reorder authMiddleware Reorder (orderedIds[])
PATCH /api/admin/mas/popup-banners/:id/toggle authMiddleware Toggle isActive
GET /api/admin/mas/popup-banners-settings authMiddleware Read settings singleton
PUT /api/admin/mas/popup-banners-settings authMiddleware Update settings singleton
Method Path Auth/role Purpose
POST /api/admin/mas/banner-templates/upload authMiddleware + adminMiddleware Upload an image (multipart banner) to S3 + create record
GET /api/admin/mas/banner-templates authMiddleware + adminMiddleware List templates (filters: category, isActive, search, tags)
GET /api/admin/mas/banner-templates/tags authMiddleware + adminMiddleware Distinct tags of active templates
GET /api/admin/mas/banner-templates/:id authMiddleware + adminMiddleware Get one template
PUT /api/admin/mas/banner-templates/:id authMiddleware + adminMiddleware Update metadata
DELETE /api/admin/mas/banner-templates/:id authMiddleware + adminMiddleware Delete from S3 + DB
PATCH /api/admin/mas/banner-templates/:id/toggle authMiddleware + adminMiddleware Toggle isActive

Event configuration — EventConfigurationRoutes mounted at /api

Method Path Auth/role Purpose
GET /api/event-configurations/public Public Active configs as a { key: value } map (website read)
GET /api/admin/mas/event-configurations authMiddleware List all configs
GET /api/admin/mas/event-configurations/:key authMiddleware Get one active config by key
POST /api/admin/mas/event-configurations authMiddleware Create config
PUT /api/admin/mas/event-configurations/:id authMiddleware Update config
DELETE /api/admin/mas/event-configurations/:id authMiddleware Delete config

League registration — LeagueRegistrationRoutes mounted at /api

Method Path Auth/role Purpose
POST /api/league-registration Public (multipart resume) Submit a registration + resume upload
GET /api/admin/league-registrations authMiddleware + adminMiddleware Paginated list (page, limit)
GET /api/admin/league-registrations/stats authMiddleware + adminMiddleware Aggregate stats (total, by college, by year)
GET /api/admin/league-registrations/export authMiddleware + adminMiddleware Export all rows
GET /api/admin/league-registrations/:id authMiddleware + adminMiddleware Get one registration
GET /api/admin/league-registrations/:id/resume-url authMiddleware + adminMiddleware Presigned (1h) resume download URL
DELETE /api/admin/league-registrations/:id authMiddleware + adminMiddleware Delete registration

Course interest — courseInterestRoutes mounted at /api/interest

Method Path Auth/role Purpose
POST /api/interest/ None (public) Submit course interest
GET /api/interest/ None Paginated list (filters: status, category)
GET /api/interest/all None List with date-range + search
GET /api/interest/stats None Aggregate counts by category/status/course
GET /api/interest/export None Export (up to 10000 rows)
PATCH /api/interest/:id None Update status/notes/assignedTo
DELETE /api/interest/:id None Delete

AI for Everyone interest — AiForEveryoneRoutes mounted at /api

Method Path Auth/role Purpose
POST /api/ai-for-everyone/interest None (public) Submit interest
GET /api/ai-for-everyone/interest None Paginated list
GET /api/ai-for-everyone/interest/stats None Statistics
GET /api/ai-for-everyone/interest/:id None Get one
PATCH /api/ai-for-everyone/interest/:id/status None Update status/notes
DELETE /api/ai-for-everyone/interest/:id None Delete

The /api/interest/* and /api/ai-for-everyone/interest/* "admin" reads/writes carry no middleware in the source — the route files note auth is to be "added later". Treat the GET/PATCH/DELETE endpoints there as effectively public today. (See gotchas.)


User journeys

Journey 1 — Admin creates and activates a sticky banner from a template

The admin uploads an image to the template library, then creates a banner, then flips the settings singleton's activeBannerId so the public site shows it. Note the two-step model: creating a banner with isActive: true is not enough to make it the one the public endpoint returns — the settings pointer (or an isDefault flag) decides.

sequenceDiagram
    autonumber
    actor Admin
    participant FE as Admin dashboard
    participant TplC as BannerTemplateController
    participant TplS as BannerTemplateService
    participant S3 as S3Service
    participant StkC as StickyBannerController
    participant StkS as StickyBannerService
    participant DB as PostgreSQL

    Admin->>FE: Upload banner image and fill name plus category
    FE->>TplC: POST /api/admin/mas/banner-templates/upload (multipart)
    TplC->>TplC: Validate file present and name present
    TplC->>S3: uploadBannerTemplate(buffer, name, mime, category)
    S3-->>TplC: returns url and s3Key
    TplC->>TplS: createBannerTemplate(metadata)
    TplS->>DB: INSERT banner_templates
    TplS-->>FE: 201 template created

    Admin->>FE: Create a sticky banner using that artwork
    FE->>StkC: POST /api/admin/mas/sticky-banners (title, colors, buttonLink)
    StkC->>StkS: createBanner(body)
    StkS->>StkS: Auto-assign displayOrder as max plus 1
    alt isDefault is true
        StkS->>DB: UPDATE sticky_banners SET isDefault false for all
    end
    StkS->>DB: INSERT sticky_banners
    StkS-->>FE: 201 banner created with new id

    Admin->>FE: Set this banner as the active one
    FE->>StkC: PUT /api/admin/mas/sticky-banners-settings (activeBannerId = newId, isEnabled true)
    StkC->>StkS: updateSettings(body)
    StkS->>StkS: getSettings loads or creates the singleton
    StkS->>DB: UPDATE sticky_banner_settings
    StkS-->>FE: 200 settings saved

Journey 2 — Public website fetches the active banner with mobile/desktop targeting

The website calls the public endpoint on every page load. The resolver walks a precedence chain (virtual default → DB pointer → isDefault row → absolute hard-coded fallback) and always returns something. Device targeting is decided by the client using the showOnMobile / showOnDesktop flags returned in settings.

sequenceDiagram
    autonumber
    participant Web as Public website
    participant StkC as StickyBannerController
    participant StkS as StickyBannerService
    participant DB as PostgreSQL

    Web->>StkC: GET /api/sticky-banners/public
    StkC->>StkS: getPublicBannerData()
    StkS->>DB: getSettings (findOne where empty, auto-create if none)
    DB-->>StkS: settings row

    alt activeBannerId equals SYSTEM_DEFAULT_50
        StkS->>StkS: Build virtual 50 percent off banner in memory
    else activeBannerId equals SYSTEM_DEFAULT_FLAGSHIP
        StkS->>StkS: Build virtual flagship banner in memory
    else activeBannerId is a real UUID
        StkS->>DB: findOne sticky_banners by id
        DB-->>StkS: banner or null
    end

    opt no banner resolved yet
        StkS->>DB: findOne where isDefault true
        DB-->>StkS: default banner or null
        opt still nothing
            StkS->>StkS: Use absolute fallback 50 percent off banner
        end
    end

    StkS-->>StkC: banners array plus settings
    StkC-->>Web: 200 with banner and settings
    Web->>Web: Render only if isEnabled and device matches showOnMobile or showOnDesktop

Journey 3 — Visitor registers for the League (with resume upload)

A public form posts multipart data including a resume. The service saves the row first, then attempts the S3 upload and patches the row with the resume key/URL. A failed upload is swallowed so the registration still succeeds. Admins later fetch a short-lived presigned URL to read the resume.

sequenceDiagram
    autonumber
    actor Visitor
    participant FE as Public form
    participant Mul as multer (5MB, pdf/doc/docx)
    participant LC as LeagueRegistrationController
    participant LS as LeagueRegistrationService
    participant S3 as S3Service
    participant DB as PostgreSQL

    Visitor->>FE: Fill details and attach resume
    FE->>Mul: POST /api/league-registration (multipart resume)
    Mul->>LC: file in memory plus body fields
    LC->>LC: Validate required fields, email regex, file present
    LC->>LS: createRegistration(data with resume buffer)
    LS->>DB: INSERT league_registrations (profiles default empty)
    DB-->>LS: saved row with id
    alt resume buffer present
        LS->>S3: uploadEventResume(buffer, id, name, type)
        alt upload ok
            S3-->>LS: s3Key and url
            LS->>DB: UPDATE row set resumeKey, resumeUrl, resumeFileName
        else upload fails
            LS->>LS: Catch and log then keep registration
        end
    end
    LS-->>FE: 201 registration created

    Note over FE,Visitor: Later — admin reviews
    participant Admin as Admin
    Admin->>LC: GET /api/admin/league-registrations/:id/resume-url
    LC->>LS: getRegistrationById(id)
    LS->>DB: findOne by id
    LC->>S3: getEventResumeSignedUrl(resumeKey, 3600)
    S3-->>LC: presigned URL valid one hour
    LC-->>Admin: 200 url and fileName

Journey 4 — Visitor expresses course interest (lead-style capture)

The public form captures a lightweight interest record with validation and de-duplication. There is no CRM Lead creation and no email is sent today — both are placeholders. Admin triage happens through the same (currently unguarded) router.

sequenceDiagram
    autonumber
    actor Visitor
    participant FE as Public website
    participant CIC as courseInterest.controller
    participant Repo as CourseInterest repository
    participant DB as PostgreSQL

    Visitor->>FE: Submit name, email, phone, interestedIn, category
    FE->>CIC: POST /api/interest/
    CIC->>CIC: Validate required fields, email regex, phone 10 digits, category enum
    CIC->>Repo: findOne by email plus interestedIn plus category
    Repo->>DB: SELECT course_interests
    alt duplicate exists
        CIC-->>FE: 409 already registered interest
    else new
        CIC->>Repo: create then save (source website, email lowercased, phone digits only)
        Repo->>DB: INSERT course_interests (status new)
        Note over CIC: Email notification is a commented-out placeholder
        Note over CIC: No CRM Lead row is created here
        CIC-->>FE: 201 thank you for your interest
    end

    Note over FE: Later — staff triage
    actor Staff
    Staff->>CIC: PATCH /api/interest/:id (status contacted, notes, assignedTo)
    CIC->>Repo: update fields
    opt status equals contacted
        CIC->>Repo: set lastContactedAt to now
    end
    Repo->>DB: UPDATE course_interests
    CIC-->>Staff: 200 updated

The AI for Everyone capture (POST /api/ai-for-everyone/interest) follows the exact same shape via AiForEveryoneControllerAiForEveryoneService, with a stricter phone regex (^[6-9]\d{9}$) and a college field instead of interestedIn/category.


Background jobs & async

There are no scheduled jobs, queues, or timers in this domain. Banner activation/expiry is entirely manual — toggling isActive, flipping isDefault, or pointing activeBannerId at a banner. None of the services use BullMQ, cron, or any time-based activation.

The only asynchronous work is S3 I/O performed inline within the request:

  • BannerTemplateService.deleteBannerTemplate / S3Service.deleteBannerTemplate — best-effort S3 delete; failures are caught/logged and DB deletion proceeds regardless.
  • LeagueRegistrationService.createRegistration — uploads the resume to S3 after the row is saved; upload failures are swallowed so the registration still returns.

Email is not sent anywhere in this domain. The course-interest controller has an explicit commented-out sendInterestNotificationEmail placeholder.


External integrations

AWS S3 is the only external dependency. Two buckets are used (defaults shown; overridable by env):

Purpose Env var Default bucket Key layout
Banner template artwork (public assets) AWS_S3_BANNER_ASSETS_BUCKET myanalyticsschool-assets banner-templates/<category>/<filename>
League resume uploads (private documents) AWS_S3_STUDENT_DOCUMENTS_BUCKET myanalytics-documents inter-iit-event/<year>/<registrationId>/<filename>

All S3 access is funnelled through src/services/S3Service.ts (per the repo convention — never raw AWS SDK calls in controllers/services). Standard AWS credentials/region env vars (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION) apply.

Upload constraints

  • Banner template upload: in-memory multer, 10 MB limit, allowed MIME image/jpeg, image/png, image/jpg, image/gif, image/webp.
  • League resume upload: in-memory multer, 5 MB limit, allowed MIME PDF / DOC / DOCX only.

Feature flags / multi-platform: there is no x-platform header handling and no environment feature flag in this domain. The only "targeting" levers are the boolean showOnMobile / showOnDesktop settings flags (interpreted client-side) and the per-banner designTemplate string. (inferred: device targeting logic lives in the consuming frontend, not the backend.)


Status lifecycles

There is no time-windowed scheduling. A banner's effective visibility is the combination of its own isActive flag, the settings singleton's master isEnabled, and whether the settings activeBannerId (or isDefault) selects it. The diagram below models a single banner's editorial lifecycle as driven by the admin endpoints.

stateDiagram-v2
    [*] --> Draft: POST create with isActive false
    [*] --> Active: POST create with isActive true

    Draft --> Active: PATCH toggle (isActive true)
    Active --> Draft: PATCH toggle (isActive false)

    Active --> Selected: PUT settings activeBannerId points here
    Selected --> Active: settings pointer moved elsewhere

    Active --> DefaultFallback: PUT update isDefault true
    DefaultFallback --> Active: another banner set as default

    state "Shown to visitors" as Shown
    Selected --> Shown: settings isEnabled true and device matches
    DefaultFallback --> Shown: no active pointer resolves and isEnabled true

    Selected --> Hidden: settings isEnabled false
    Active --> Hidden: not selected and not default

    Draft --> [*]: DELETE
    Active --> [*]: DELETE
    Selected --> [*]: DELETE (settings still points at a stale id)

Key points:

  • No "scheduled" or "expired" state exists — there are no startDate/endDate columns. (Confirmed across all entities and services.)
  • A banner can be isActive yet never shown, because the public resolver returns a single banner chosen by the settings pointer / default flag, not the full active list.
  • Deleting a banner does not clear a dangling activeBannerId in settings; the public resolver simply falls through to the isDefault row or the hard-coded fallback.

Edge cases, limits & gotchas

  1. Inconsistent authorization.
  2. Sticky/pop-up banner admin routes and event-config admin routes use authMiddleware only — any authenticated user (not just ADMIN) can create/update/delete.
  3. Banner-template and league-registration admin routes correctly add adminMiddleware.
  4. Hardening note (internal). A security/hardening observation for this area is tracked in the team's private notes (internal/security-and-hardening-notes.md) and is intentionally not published on this site.

  5. Public banner endpoint always returns a banner. Because of the multi-step fallback (virtual default → DB pointer → isDefault → absolute hard-coded SYSTEM_DEFAULT_50), the website can never get an empty result — even with zero banners in the DB. Hiding banners must be done via settings.isEnabled = false on the client side, not by emptying the table.

  6. Virtual banners are not in the DB. IDs SYSTEM_DEFAULT_50, SYSTEM_DEFAULT_FLAGSHIP (sticky) and SYSTEM_DEFAULT_ORIGINAL, SYSTEM_DEFAULT_MAS101, SYSTEM_DEFAULT_MAS_SPECIAL, SYSTEM_DEFAULT_MAS_PROFESSIONAL (pop-up) are matched as magic strings in activeBannerId. Admin "active list" reads will never include them, but the public endpoint can return them. Hard-coded promo content (Razorpay link rzp.io/rzp/..., copy) lives in the service code and changes require a deploy.

  7. displayMode / transitionDuration exist only on sticky settings. The pop-up settings entity is simpler (isEnabled + mobile/desktop only). The sticky display mode (single | multiple | auto_scroll) and transitionDuration are interpreted by the frontend; the backend never returns more than the resolved single banner today.

  8. Default-banner uniqueness is imperative, not a constraint. Setting isDefault: true on create/update issues UPDATE ... SET isDefault = false across all rows first. There is no DB-level partial unique index, so a direct DB write or a race could leave multiple defaults; the public resolver would then pick whichever findOne returns.

  9. reorderBanners uses 0-based indices (displayOrder = index), while createBanner auto-assigns max + 1. Mixing the two can produce overlapping displayOrder values; ordering then falls back to createdAt DESC.

  10. League source filter is silently ignored. LeagueRegistrationController reads a source query param and passes it to getAllRegistrations / getRegistrationStats / exportAllRegistrations, but the service methods don't accept a source argument — so the filter is a no-op. (Bug-shaped.)

  11. League resume upload failures are swallowed. A registration with a failed S3 upload returns 201 with null resumeKey/resumeUrl; the admin's /resume-url call will then fail or return nothing. There is no retry.

  12. Course interest de-dup is per (email + interestedIn + category). The same person can create many interest rows across different courses/categories; the 409 only blocks an exact triple repeat.

  13. No CRM lead linkage. Despite the lead-like assignedTo / status / lastContactedAt fields, neither course-interest nor AI-for-everyone capture creates a Sales CRM Lead or calls LeadEventService. Any pipeline integration would have to be added. (See Related docs.)

  14. TypeORM auto-sync is ON. Adding a column to any of these entities mutates the live table automatically — be deliberate with type changes (e.g. widening phone varchar lengths differ: course_interests.phone is varchar(15), league_registrations.phone is varchar(20)).


  • Sales CRM & Leads — where interest captures would feed if lead linkage were wired up; shares the assignedTo / status / lastContactedAt triage semantics.
  • AI Platform & LLM Gateway — the "AI for Everyone" program these interest forms market.
  • Uploads & S3S3Service, bucket layout, presigned URLs, and the AWS_S3_BANNER_ASSETS_BUCKET / AWS_S3_STUDENT_DOCUMENTS_BUCKET conventions used here.
  • Courses & Enrollment — the cohorts/capsules/accelerators referenced by CourseInterest.category and interestedIn.