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:
| Route | Component | Purpose |
|---|
/:tokenId | PortalLanding | Token validation and branding setup |
/:tokenId/dashboard | ProjectDashboard | Stakeholder-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
- An admin creates a portal access link via
CreateTokenModal in the main app.
- A unique token document is written to the
portalTokens Firestore collection.
- The generated URL (
https://impact.grantmaster.ai/:tokenId) is shared with the stakeholder.
- When the stakeholder visits the URL,
PortalLanding calls the validatePortalToken Cloud Function.
- On success, the validated project data and branding are stored in
sessionStorage as portal_${tokenId}.
- 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:
| Field | Type | Description |
|---|
projectId | string | Associated project |
organizationId | string | Owning organization |
stakeholderName | string | Display name for the stakeholder |
stakeholderEmail | string? | Optional, used for notifications |
stakeholderType | string | funder, partner, board, auditor |
visibility | object | 12 boolean visibility flags |
expiresAt | Timestamp | Automatic expiration date |
isActive | boolean | Can be set to false to revoke |
notes | string? | Internal admin notes |
createdBy | string | UID of creating admin |
createdAt | Timestamp | Creation 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):
| Function | Limit |
|---|
validatePortalToken | 10 req/min |
postPortalComment | 5 req/min |
getPortalUploadUrl | 5 req/min |
submitPortalReport | 5 req/min |
updatePortalSession | 60 req/min |
endPortalSession | 60 req/min |
acknowledgePortalReport | 10 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.
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
| Collection | Purpose |
|---|
portalTokens | Access token documents (one per stakeholder link) |
portalSessions | Session tracking for audit trail |
portalComments | Threaded stakeholder comments |
portalSubmissions | Document submission records |
portalReportAcknowledgments | Report sign-off records |
systemEvents | Notification 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:
| Section | Icon | Purpose |
|---|
| Access Links | Link2 | Create, view, and revoke portal tokens |
| Submissions | Upload | Review stakeholder document uploads |
| Messages | MessageSquare | Read and reply to stakeholder comments |
| Analytics | BarChart3 | Engagement 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 Type | Display |
|---|
invalid | ”This link is not valid” |
expired | ”This link has expired” |
revoked | ”This link has been revoked” |
network | Network error with retry |
unknown | Generic 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:
| Section | Visibility Gate |
|---|
| Quick stats grid | Always shown |
| Budget progress | showBudgetUtilization or showBudgetDetails |
| Timeline | showTimeline |
| Milestones | showMilestones |
| Impact metrics | showImpactMetrics |
| Next report due | showMilestones |
| Reports | showReports |
| Document submission | allowReportSubmission |
| Team members | showTeamMembers |
| Comments | allowComments |
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/:
| Service | File | Responsibility |
|---|
portalTokenService | PortalTokenService.ts | CRUD for token documents; generates portal URLs |
portalSubmissionsService | PortalSubmissionsService.ts | List and review submission records |
portalCommentsService | PortalCommentsService.ts | Read 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:
- Start
npm run dev:all (emulators + main app)
- Start
npm run dev inside portal/ (portal Vite server)
- Create a token in the main app → copy the URL → replace the domain with
localhost:PORT