Skip to main content

Documentation Index

Fetch the complete documentation index at: https://grantmaster.dev/llms.txt

Use this file to discover all available pages before exploring further.

Grantor Portal

Overview

The Grantor Portal is a secure, token-authenticated external web application that allows stakeholders (grantors, partners, board members, auditors) to view project progress, submit documents, and communicate with the GrantMaster organization — without needing a GrantMaster account. The portal is a separate Vite application (portal/) that runs on impact.grantmaster.ai and communicates with the main Firebase project exclusively through Cloud Functions. The main GrantMaster application manages portal access from the Grantors page (Portal tab), while stakeholders interact through the standalone portal app.

Architecture

Main App (grantmaster.app)         External Portal (impact.grantmaster.ai)
────────────────────────────       ────────────────────────────────────────
src/features/grantors/             portal/src/
  components/portal/                 pages/PortalLanding.tsx
    GrantorPortalTab.tsx               pages/ProjectDashboard.tsx
    CreateTokenModal.tsx               services/portalApi.ts
    RevokeTokenModal.tsx               App.tsx (router)
    ReviewSubmissionModal.tsx
    AccessLinksSection.tsx
    SubmissionsSection.tsx         Firebase Cloud Functions
    MessagesSection.tsx            functions/src/portal/index.ts
    AnalyticsSection.tsx             validatePortalToken
  hooks/useOrgPortal.ts              postPortalComment
  services/ (via projects/)          acknowledgePortalReport
    portalTokenService.ts            updatePortalSession
    portalSubmissionsService.ts      endPortalSession
    portalCommentsService.ts         getPortalUploadUrl
                                     submitPortalReport

Portal URL and Routing

The portal app uses path-based token routing:
RouteComponentPurpose
/:tokenIdPortalLandingToken validation and branding setup
/:tokenId/dashboardProjectDashboardStakeholder-facing project view
The token ID is embedded in the URL path, not as a query parameter. This enables clean links for stakeholders and avoids exposing tokens in server logs as query strings.

Token-Based Authentication

How It Works

  1. An admin creates a portal access link via CreateTokenModal in the main app.
  2. A unique token document is written to the portalTokens Firestore collection.
  3. The generated URL (https://impact.grantmaster.ai/:tokenId) is shared with the stakeholder.
  4. When the stakeholder visits the URL, PortalLanding calls the validatePortalToken Cloud Function.
  5. On success, the validated project data and branding are stored in sessionStorage as portal_${tokenId}.
  6. The user is redirected to /:tokenId/dashboard, which reads from sessionStorage to render the dashboard.

Token Lifecycle

Created → Active → [Expired (date) | Revoked (admin action)]
Tokens are validated by two conditions checked server-side:
  • isActive === true
  • expiresAt (Firestore Timestamp) is in the future

Token Data Structure

Tokens are stored in the portalTokens Firestore collection with:
FieldTypeDescription
projectIdstringAssociated project
organizationIdstringOwning organization
stakeholderNamestringDisplay name for the stakeholder
stakeholderEmailstring?Optional, used for notifications
stakeholderTypestringfunder, partner, board, auditor
visibilityobject12 boolean visibility flags
expiresAtTimestampAutomatic expiration date
isActivebooleanCan be set to false to revoke
notesstring?Internal admin notes
createdBystringUID of creating admin
createdAtTimestampCreation timestamp

Visibility Settings

Each token carries a PortalVisibility object with 12 boolean flags that control exactly what the stakeholder can see: Data Visibility
  • showBudgetUtilization — Budget utilization percentage
  • showBudgetDetails — Budget amounts in currency
  • showBudgetLines — Individual budget line items
  • showMilestones — Milestones and deadlines
  • showTimeline — Project timeline and dates
  • showTeamMembers — Team member names and roles
  • showComplianceStatus — Compliance status indicator
  • showImpactMetrics — Impact and outcome metrics
Reports
  • showReports — Access to generated reports
Interaction
  • allowComments — Post and view threaded comments
  • allowReportAcknowledgment — Acknowledge/sign off on reports
  • allowReportSubmission — Upload documents to the portal

Cloud Functions

All portal API calls go through Firebase Cloud Functions in functions/src/portal/index.ts. The portal app communicates exclusively through these callable functions — it has no direct Firestore access. CORS: Restricted to https://impact.grantmaster.ai and https://grantmaster-portal.web.app. Rate Limiting: In-memory per-IP rate limiting (resets on cold start):
FunctionLimit
validatePortalToken10 req/min
postPortalComment5 req/min
getPortalUploadUrl5 req/min
submitPortalReport5 req/min
updatePortalSession60 req/min
endPortalSession60 req/min
acknowledgePortalReport10 req/min

validatePortalToken

Validates the token, reads filtered project data, and returns everything the portal needs to render the dashboard. Data assembly:
  • Project base data from projects collection
  • Milestones from reportingDeadlines collection
  • Team members from employees collection (via project.teamIds)
  • Organization branding from organizations collection
  • Reports from generatedReports collection (if showReports is enabled)
  • Compliance status derived from budget utilization: >100%non_compliant, >90%at_risk, otherwise compliant
The getFilteredProjectData() helper applies the token’s visibility flags before returning data, so the Cloud Function never exposes fields the token doesn’t permit. A session document is created in portalSessions for audit purposes, and a systemEvent is emitted to trigger an internal notification to the organization.

postPortalComment

Writes a new comment to the portalComments collection. Associates the comment with the token’s stakeholderName rather than a user account. Supports threaded replies via parentId. Emits a systemEvent for admin notification.

acknowledgePortalReport

Creates a signed acknowledgment record in portalReportAcknowledgments. Records the stakeholder name, timestamp, and report ID. Emits a systemEvent.

updatePortalSession / endPortalSession

Track session activity in portalSessions for audit compliance. updatePortalSession updates the last-seen timestamp and page view count. endPortalSession marks the session as ended.

getPortalUploadUrl

Generates a signed Firebase Storage upload URL for document submission.
  • TTL: 15 minutes
  • Max file size: 50 MB
  • Files stored at portals/{orgId}/{projectId}/submissions/{filename}

submitPortalReport

Creates a submission record in portalSubmissions after a file has been uploaded. Status starts as submitted. Emits a systemEvent that triggers admin notification.

Firestore Collections

CollectionPurpose
portalTokensAccess token documents (one per stakeholder link)
portalSessionsSession tracking for audit trail
portalCommentsThreaded stakeholder comments
portalSubmissionsDocument submission records
portalReportAcknowledgmentsReport sign-off records
systemEventsNotification triggers (read by internal notification dispatcher)

Admin Management (Main App)

Portal Tab Location

The portal management UI lives in the main GrantMaster app under Grantors → Portal tab. The GrantorPortalTab component provides a four-section interface via SecondarySidebar:
SectionIconPurpose
Access LinksLink2Create, view, and revoke portal tokens
SubmissionsUploadReview stakeholder document uploads
MessagesMessageSquareRead and reply to stakeholder comments
AnalyticsBarChart3Engagement metrics and token usage
The sidebar displays live badge counts: active token count, pending submission count (amber badge), and unread message count (primary badge).

useOrgPortal Hook

src/features/grantors/hooks/useOrgPortal.ts is the single source of truth for the Portal tab. It aggregates data across all projects in parallel:
const [tokens, submissionsResult, unreadComments] = await Promise.all([
  portalTokenService.getTokensByOrganization(orgId),
  portalSubmissionsService.listByOrganization(orgId),
  portalCommentsService.getOrgUnreadComments(orgId),
]);
Exposed state: tokens, submissions, unreadComments, projectMap, projects, loading, error Badge counts: activeTokenCount (active and not expired), pendingSubmissionsCount (status === submitted), unreadMessagesCount Actions: createToken, revokeToken, reviewSubmission, postComment, markAsRead, refresh

Creating a Token

CreateTokenModal collects:
  • Project (required)
  • Stakeholder type (funder / partner / board / auditor)
  • Stakeholder name (required)
  • Stakeholder email (optional, for notifications)
  • Expiry: 7d / 30d / 90d / 6 months / 1 year / 2 years
  • Visibility settings (12 boolean checkboxes, grouped by category)
  • Internal notes
On success, the modal transitions to a confirmation state that shows the generated URL with copy and open-in-new-tab buttons. The URL format is https://impact.grantmaster.ai/:tokenId.

Reviewing Submissions

Submissions can be moved through a review workflow:
submitted → under_review → accepted | rejected
ReviewSubmissionModal allows admins to set the status and add review notes.

Stakeholder-Facing Portal App

Tech Stack

The portal app at portal/ is a separate Vite + React application with:
  • Firebase SDK (Functions only — no direct Firestore access)
  • i18next for internationalization (5 languages: en, de, es, fr, nl)
  • html2canvas for PDF export
  • BrandedLayout wrapper that applies organization colors from CSS variables

PortalLanding Page

Handles token validation with error states:
Error TypeDisplay
invalid”This link is not valid”
expired”This link has expired”
revoked”This link has been revoked”
networkNetwork error with retry
unknownGeneric error
On success: stores token data in sessionStorage, applies branding CSS variables, redirects to /:tokenId/dashboard.

ProjectDashboard Page

The main stakeholder view. Reads token data from sessionStorage and renders sections conditionally based on the token’s PortalVisibility flags:
SectionVisibility Gate
Quick stats gridAlways shown
Budget progressshowBudgetUtilization or showBudgetDetails
TimelineshowTimeline
MilestonesshowMilestones
Impact metricsshowImpactMetrics
Next report dueshowMilestones
ReportsshowReports
Document submissionallowReportSubmission
Team membersshowTeamMembers
CommentsallowComments
The page also supports PDF export via html2canvas.

Services (Main App Side)

The main app accesses portal data through three services located in src/features/projects/services/:
ServiceFileResponsibility
portalTokenServicePortalTokenService.tsCRUD for token documents; generates portal URLs
portalSubmissionsServicePortalSubmissionsService.tsList and review submission records
portalCommentsServicePortalCommentsService.tsRead comments, post replies, mark as read
portalTokenService.getPortalUrl(tokenId) returns the full stakeholder URL.

Notifications

The portal system uses an event-driven notification approach. Cloud Functions write systemEvent documents to the systemEvents Firestore collection. The internal notification dispatcher reads these events and dispatches notifications to organization admins for:
  • Stakeholder portal visit (on validatePortalToken)
  • New comment from stakeholder (on postPortalComment)
  • Report acknowledged by stakeholder (on acknowledgePortalReport)
  • Document submitted by stakeholder (on submitPortalReport)

Security Considerations

  • Tokens are cryptographically unique IDs (Firestore auto-ID)
  • All portal data passes through Cloud Functions — stakeholders never have direct Firestore access
  • Visibility flags are enforced server-side in getFilteredProjectData(), not just client-side
  • Rate limiting prevents brute-force token guessing
  • Session tracking provides an audit trail of all portal activity
  • Upload URLs expire after 15 minutes

Development Notes

In development, the portal app connects to the Firebase Functions emulator on port 5001 (VITE_FUNCTIONS_EMULATOR=true in portal/.env.development). The main app and portal app share the same Firebase project but run as separate Vite processes. To test the full portal flow locally:
  1. Start npm run dev:all (emulators + main app)
  2. Start npm run dev inside portal/ (portal Vite server)
  3. Create a token in the main app → copy the URL → replace the domain with localhost:PORT