Documentation Index
Fetch the complete documentation index at: https://grantmaster.dev/llms.txt
Use this file to discover all available pages before exploring further.
Shared Package Architecture
Purpose
@grantmaster/shared is a pure TypeScript package that lives in the packages/shared/ workspace. Its sole purpose is to share code that must be identical across two runtime environments:
- Frontend (
src/) — the React web application
- Cloud Functions (
functions/) — Firebase Cloud Functions (Node.js)
Because it is consumed by both environments it has a strict rule: no Firebase imports. Firebase SDKs differ between the browser client and the Admin SDK used in Functions. Any Firebase-specific code lives in the consumer package, not here. The only runtime dependency is zod (for schema definitions that are validated in both environments).
Workspace Setup
GrantMaster uses npm workspaces. The root package.json declares:
{
"workspaces": ["packages/*", "functions"]
}
This means packages/shared/ and functions/ are both workspace members. The frontend app at src/ is the workspace root itself.
Both consumers reference the package by name:
// functions/package.json
{
"dependencies": {
"@grantmaster/shared": "*"
}
}
The * version resolves to the local workspace copy. No publishing to npm is required.
Package Structure
packages/shared/
├── package.json # Package manifest with named exports
└── src/
├── index.ts # Barrel re-export of all sub-modules
├── types/
│ ├── index.ts
│ ├── enums.ts # Domain enums and constants
│ ├── common.ts # Base document interfaces
│ ├── entities.ts # Minimal entity shapes for Functions
│ └── intelligence.ts # Cross-org analytics types
├── schemas/
│ ├── index.ts
│ └── common.schema.ts # Zod schemas for shared validation
├── errors/
│ ├── index.ts
│ ├── AppError.ts # Error hierarchy base classes
│ └── domain.ts # Domain-specific error classes
├── events/
│ └── index.ts # SystemEventType enum and typed payloads
└── config/
├── index.ts
└── entitlements.ts # Feature flags and subscription limits
Named Exports (package.json)
{
"exports": {
".": "./src/index.ts",
"./events": "./src/events.ts",
"./schemas": "./src/schemas/index.ts",
"./errors": "./src/errors/index.ts",
"./events": "./src/events/index.ts",
"./config": "./src/config/index.ts"
}
}
Consumers can import from the root or from a named sub-path:
import { SystemRole } from '@grantmaster/shared';
import { SystemRole } from '@grantmaster/shared/types';
What Gets Shared
Types (@grantmaster/shared/types)
enums.ts — Domain Enumerations and Constants
| Export | Description |
|---|
SystemRole | SUPER_ADMIN, ADMIN, MANAGER, MEMBER, AUDITOR |
Permission | 200+ fine-grained permission strings |
SubscriptionTier | POTENTIAL, PROFESSIONAL, ULTIMATE |
ProjectPhase | Project lifecycle phases |
GrantorType | Government, Foundation, Corporate, Individual, Other |
ORG_SIZE_THRESHOLDS | Object with SMALL, MEDIUM, LARGE numeric thresholds |
DATA_CATEGORIES | Constants for data classification |
SystemRole and Permission are the most heavily used — they appear in Firestore security rules, Cloud Function auth checks, and frontend RBAC alike.
common.ts — Base Document Interfaces
interface BaseDocument {
id: string;
organizationId: string;
createdAt: TimestampLike;
updatedAt: TimestampLike;
}
interface AuditedDocument extends BaseDocument {
createdBy: string;
createdByName: string;
updatedBy: string;
updatedByName: string;
}
TimestampLike is a structural type that matches both firebase/firestore.Timestamp and plain { seconds: number; nanoseconds: number } objects, so it works without importing Firebase.
All Firestore documents that follow the platform convention extend one of these two interfaces.
entities.ts — Minimal Entity Shapes
Contains trimmed-down interfaces for entities that Cloud Functions need to read but do not need the full frontend type for:
ProjectBase — id, organizationId, name, phase, budget
EmployeeBase — id, organizationId, displayName, email, systemRole
These exist to avoid shipping large frontend-only fields (computed properties, UI state) into the Functions bundle.
intelligence.ts — Cross-Org Analytics
Types used by the intelligence platform for aggregating data across organisations. These are consumed by Cloud Functions that power the Superadmin analytics features.
Schemas (@grantmaster/shared/schemas)
Zod schemas validated identically on both frontend (form validation) and backend (Cloud Function input validation):
| Schema | Validates |
|---|
uuidSchema | RFC 4122 UUID strings |
organizationIdSchema | Non-empty string (Firestore document id format) |
FirestoreTimestampLike | Duck-type check for Timestamp-shaped objects |
Address | Street, city, state/province, postal code, country |
Attachment | File attachment metadata (name, url, mimeType, size) |
BankAccount | Bank account details (accountNumber, routingNumber, bankName) |
Rule: Do not add Firebase-specific types (e.g. DocumentReference, Timestamp) as schema field types. Use FirestoreTimestampLike or plain strings instead.
Errors (@grantmaster/shared/errors)
A class hierarchy for typed error handling that is consistent across the frontend and Cloud Functions:
AppError (base)
├── UserError # 4xx — caused by user input or state
│ ├── ValidationError
│ ├── AuthenticationError
│ ├── AuthorizationError
│ └── NotFoundError
└── SystemError # 5xx — internal failures
├── FirestoreError
├── StorageError
├── NetworkError
├── RateLimitError
├── BusinessLogicError
├── ExternalAPIError
├── AIServiceError
└── EmailServiceError
Domain errors in domain.ts build on the base classes for platform-specific scenarios:
| Class | When to throw |
|---|
RateLimitExceededError | Quota exceeded (API calls, AI generations, agent credits) |
TenantIsolationError | Cross-org data access attempt detected |
FeatureNotAvailableError | Feature not available on the organisation’s subscription tier |
All AppError subclasses carry an ErrorSeverity (INFO, WARNING, ERROR, CRITICAL) and an ErrorCategory for structured logging.
Events (@grantmaster/shared/events)
The event system is the backbone of the platform’s audit trail and notification pipeline. See docs/engineering/architecture/base-service-and-eventbus.md for the full EventBus rule.
SystemEventType
A string enum with 100+ event types organised by domain:
| Domain group | Examples |
|---|
| Journals | JOURNAL_SUBMITTED, JOURNAL_APPROVED, JOURNAL_REJECTED |
| Expenses | EXPENSE_SUBMITTED, EXPENSE_APPROVED, EXPENSE_FLAGGED |
| Compliance | COMPLIANCE_OVERDUE, COMPLIANCE_COMPLETED |
| Budget | BUDGET_THRESHOLD_80, BUDGET_THRESHOLD_90, BUDGET_EXCEEDED |
| Project lifecycle | PROJECT_CREATED, PROJECT_ARCHIVED, PROJECT_PHASE_CHANGED |
| Employee | EMPLOYEE_ONBOARDED, EMPLOYEE_OFFBOARDED, EMPLOYEE_ROLE_CHANGED |
| Grants | GRANT_APPLICATION_SUBMITTED, GRANT_AWARDED, GRANT_REJECTED |
| Agent | AGENT_RUN_STARTED, AGENT_RUN_COMPLETED, AGENT_RUN_FAILED |
| Credits | CREDITS_CONSUMED, CREDITS_LOW, CREDITS_EXHAUSTED |
| Impact/M&E | ME_INDICATOR_UPDATED, ME_REPORT_SUBMITTED |
SystemEvent<T> Interface
interface SystemEvent<T = unknown> {
type: SystemEventType;
organizationId: string;
userId: string;
severity: EventSeverity;
timestamp: TimestampLike;
payload: T;
metadata?: Record<string, unknown>;
}
Each event type has a corresponding typed payload interface (e.g. JournalEventPayload, ExpenseEventPayload).
Type Guards
isJournalEvent(event: SystemEvent): event is SystemEvent<JournalEventPayload>
isExpenseEvent(event: SystemEvent): event is SystemEvent<ExpenseEventPayload>
// ... one per domain group
requiresPersistence(eventType)
Returns true for event types that must be written to Firestore for the audit trail. These include: audit events, compliance events, approval workflow events, budget threshold events, employee lifecycle events, project lifecycle events, agent events, credit events, and M&E events. Read operations and trivial updates return false.
Config (@grantmaster/shared/config)
Subscription tier entitlements and feature flags used by both the frontend (to gate UI) and Cloud Functions (to enforce limits server-side).
Feature Enum
40+ feature identifiers, e.g. AI_GRANTS_DISCOVERY, ADVANCED_REPORTING, CUSTOM_ROLES, MULTI_ORG, AGENT_SYSTEM.
TierLimits and TIER_LIMITS
interface TierLimits {
maxUsers: number;
maxProjects: number;
maxStorageGB: number;
maxApiCallsPerMonth: number;
maxAIGenerationsPerMonth: number;
maxAgentCreditsPerMonth: number;
maxAgentRunsPerMonth: number;
}
| Tier | Max users | Max projects | Storage |
|---|
POTENTIAL | 3 | 10 | 5 GB |
PROFESSIONAL | 15 | 50 | 50 GB |
ULTIMATE | 25 | unlimited | unlimited |
Helper Functions
| Function | Description |
|---|
getMinimumTierForFeature(feature) | Lowest tier that includes this feature |
tierIncludesFeature(tier, feature) | Boolean check |
getFeaturesForTier(tier) | All features available at a tier |
getLimitsForTier(tier) | TierLimits object for a tier |
How to Add New Shared Code
Decision checklist
Add code to @grantmaster/shared only if it meets all of the following criteria:
If any criterion fails, put the code in the consumer package instead.
Steps
-
Choose the right sub-module based on what you are adding:
| What you are adding | Sub-module |
|---|
| A domain enum or constant | owning shared or feature contract module |
| A base document interface | owning schema or shared contract module |
| A minimal entity shape for Functions | owning shared contract module |
| A Zod validation schema | src/schemas/common.schema.ts |
| A new error class | src/errors/AppError.ts or src/errors/domain.ts |
| A new event type | src/events/index.ts |
| A feature flag or tier limit | src/config/entitlements.ts |
-
Write the code in the chosen file. Follow the existing style — use
export on all public symbols.
-
Re-export from the sub-module’s
index.ts if the file has one.
-
Verify the root barrel (
src/index.ts) re-exports the sub-module. It should already, since it re-exports ./types, ./schemas, ./errors, ./events, and ./config wholesale.
-
Run the TypeScript compiler across both consumers to catch any issues:
# From repo root
npm run build # checks frontend
npm run build --workspace=functions # checks Cloud Functions
-
Do not add new runtime dependencies without team discussion. The package intentionally has only one runtime dependency (
zod). Adding Firebase, Lodash, or similar libraries defeats the purpose of the package being environment-agnostic.
Adding a New Event Type
Events require more steps than other additions because each type needs a payload interface and (potentially) persistence registration:
- Add the new value to
SystemEventType in src/events/index.ts.
- Define a typed payload interface (e.g.
MyDomainEventPayload).
- Extend the relevant type guard or add a new one.
- Update
requiresPersistence() if the event must be written to Firestore.
- In the consuming service, emit via
EventBus.emit(SystemEventType.MY_EVENT, payload) following the EventBus rule in docs/engineering/architecture/base-service-and-eventbus.md.
Adding a New Error Class
- Decide whether it is a
UserError (client mistake) or SystemError (internal failure).
- Extend the appropriate base class in
src/errors/AppError.ts (for general errors) or src/errors/domain.ts (for platform-specific errors).
- Set a meaningful
severity and category in the constructor.
- Export from
src/errors/index.ts.
Dependency Rule Summary
Allowed in @grantmaster/shared | Not allowed |
|---|
| Pure TypeScript | Firebase client SDK |
| Zod schemas | Firebase Admin SDK |
| Enums and constants | @tanstack/react-query |
| Structural type definitions | React or JSX |
| Error class hierarchies | Browser globals (window, fetch) |
| Event type enumerations | Node built-ins (fs, path, crypto) |