Architecture
Detailed technical architecture of PBS Knowledge.
System Overview
┌──────────────────────────────────────────────────────────────────┐
│ NGINX (Reverse Proxy) │
│ SSL Termination, Static Files │
└─────────────────────────────┬────────────────────────────────────┘
│
┌───────────────────┼───────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Frontend │ │ Backend │ │ Worker │
│ (Svelte 5) │ │ (Fastify) │ │ (BullMQ) │
│ │ │ │ │ │
│ • Dashboard │ │ • REST API │ │ • Jobs │
│ • Planning │ │ • Auth │ │ • Sync │
│ • Admin │ │ • Services │ │ • Email │
└─────────────────┘ └────────┬────────┘ └────────┬────────┘
│ │
▼ ▼
┌──────────────────────────────────────────────────────────────────┐
│ Data Layer │
│ │
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
│ │ TerminusDB │ │ Redis │ │ Firebase │ │
│ │ (Graph DB) │ │ (Cache/Queue)│ │ (Auth/Docs) │ │
│ └───────────────┘ └───────────────┘ └───────────────┘ │
└──────────────────────────────────────────────────────────────────┘
Frontend Architecture
Technology Stack
- Framework: Svelte 5 with runes
- Build Tool: Vite 6
- Styling: Tailwind CSS 3
- Forms: Felte + AJV
- Visualization: Cytoscape.js
Component Structure
frontend/src/
├── lib/
│ ├── api.ts # Centralized API client
│ ├── components/ # Reusable UI components
│ │ ├── common/ # Generic components
│ │ ├── forms/ # Form components
│ │ ├── faculty/ # Faculty-specific
│ │ └── milestones/ # Milestone components
│ ├── forms/ # Schema-driven form system
│ │ ├── fields/ # Field renderers
│ │ ├── validators/ # Custom validators
│ │ └── loaders/ # Data loaders
│ ├── stores/ # Svelte stores
│ └── types/ # TypeScript types
├── pages/ # Full-page components
└── routes/ # Route components
State Management
State is managed through:
- Svelte Stores - Global application state
- Component State - Local component state
- URL State - Route parameters and query strings
- Form State - Felte form management
API Communication
All API calls go through lib/api.ts:
// Centralized fetch with auth
const api = {
get: path => fetch(`/api${path}`, { headers: authHeaders() }),
post: (path, data) => fetch(`/api${path}`, { method: 'POST', body: JSON.stringify(data) }),
// ...
};
Backend Architecture
Technology Stack
- Framework: Fastify 5
- Language: TypeScript 5
- Database Client: Custom TerminusDB client
- Job Queue: BullMQ 5
- Authentication: SAML 2.0 + Firebase
Layered Architecture
HTTP Request
│
▼
┌─────────────┐
│ Routes │ ← Request handling, validation
└─────────────┘
│
▼
┌─────────────┐
│ Services │ ← Business logic
└─────────────┘
│
▼
┌─────────────┐
│ Data Access │ ← TerminusDB GraphQL
└─────────────┘
Route Structure
backend/src/routes/
├── auth.ts # Authentication endpoints
├── people.ts # Person CRUD
├── courses.ts # Course management
├── publications.ts # Publication endpoints
├── degrees.ts # Degree configuration
├── milestones.ts # Milestone management
├── studentPlans.ts # Academic planning
├── dartmouthSync/ # Dartmouth API integration
└── ...
Service Pattern
Services encapsulate business logic:
class PersonService {
constructor(fastify, request) {
this.db = fastify.terminusdb;
this.user = request.user;
}
async getPerson(id: string) {
// Business logic here
}
}
Database Architecture
TerminusDB
Graph database with:
- Classes - Entity types (Person, Course, etc.)
- Properties - Fields on entities
- Links - Relationships between entities
- Enums - Controlled vocabularies
Schema Organization
{
"@context": {...},
"@graph": [
{
"@id": "@schema:Person",
"@type": "Class",
"first_name": "xsd:string",
"email": "xsd:string",
"affiliations": {
"@type": "Set",
"@class": "@schema:Lab"
}
}
]
}
GraphQL Interface
TerminusDB exposes GraphQL:
query {
Person(filter: { email: { eq: "user@dartmouth.edu" } }) {
_id
first_name
last_name
affiliations {
name
}
}
}
Authentication Flow
SAML SSO
User → Login Page → Dartmouth IdP → SAML Response → Backend → Session
- User clicks login
- Redirected to Dartmouth SSO
- User authenticates
- SAML assertion returned
- Backend validates and creates session
- JWT issued for API access
Authorization
Permissions checked at multiple levels:
- Route middleware (authentication)
- Service layer (authorization)
- Database layer (field-level)
Job Processing
BullMQ Architecture
Event/Schedule → Job Queue (Redis) → Worker → Result
Queue Types
| Queue | Purpose |
|---|---|
email | Send transactional emails |
sync | Data synchronization |
publication | Publication import |
backup | Database backups |
Worker Process
const worker = new Worker(
'email',
async job => {
const { to, template, data } = job.data;
await emailService.send(to, template, data);
},
{ connection: redis }
);
Deployment Architecture
Container Structure
services:
frontend: # Svelte app via NGINX
backend: # Fastify API
worker: # BullMQ processor
terminusdb: # Graph database
redis: # Cache and queues
nginx: # Reverse proxy
Network Flow
Internet → NGINX:443 → Backend:3000 → TerminusDB:6363
→ Frontend assets
→ Static files
Security Architecture
Defense Layers
- NGINX - Rate limiting, headers
- Backend - Authentication, validation
- Services - Authorization
- Database - Schema validation
FERPA-Compliant Hybrid Storage
PBS Knowledge uses a hybrid storage architecture to meet FERPA requirements for student educational records:
┌─────────────────────────────────────────────────────────────┐
│ Firebase Firestore │
│ (Encrypted at Rest - Google Cloud) │
│ │
│ ┌─────────────────────────┐ ┌─────────────────────────┐ │
│ │ student_grades │ │ audit_logs │ │
│ │ ──────────────── │ │ ──────────────── │ │
│ │ • grade: "A" │ │ • userId │ │
│ │ • gpa: 3.85 │ │ • resourceType │ │
│ │ • enrollment_ref │ │ • resourceId │ │
│ │ • updated_at │ │ • action, timestamp │ │
│ └─────────────────────────┘ └─────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
│ Links via enrollment_ref
▼
┌─────────────────────────────────────────────────────────────┐
│ TerminusDB │
│ (General Academic Data - Graph DB) │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ CourseEnrollment │ │
│ │ ───────────────── │ │
│ │ • student (ref to Person) │ │
│ │ • section (ref to Course) │ │
│ │ • term: "25W" │ │
│ │ • status: "enrolled" │ │
│ │ • (NO grade - stored in Firestore) │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Why Hybrid?
| Concern | Solution |
|---|---|
| Grades need encryption at rest | Firestore provides automatic AES-256 encryption |
| FERPA audit trail required | All grade access logged to audit_logs collection |
| Compliance certifications | Firebase has SOC 2, ISO 27001 |
| Data minimization | Sensitive grades separated from general enrollment data |
Access Control Middleware
All student data endpoints use the canAccessStudent middleware:
// backend/src/middleware/studentAccess.ts
export async function canAccessStudent(request, reply) {
const { id: studentId } = request.params;
const user = request.user;
// Only allow: the student themselves, their advisor, or admin
const canAccess =
user.personId === studentId ||
user.role === 'admin' ||
(await isAdvisorOf(user.personId, studentId));
if (!canAccess) {
return reply.code(403).send({ error: 'Access denied' });
}
}
Audit Logging
Every access to FERPA-protected data is logged:
// backend/src/middleware/auditMiddleware.ts
export function auditStudentAccess(resourceType: string) {
return async (request, reply) => {
reply.addHook('onSend', async () => {
await request.server.auditService.logAccess({
userId: request.user?.id,
resourceType, // 'enrollment', 'grade', 'transcript', 'degree-progress'
resourceId: request.params.id,
action: 'view',
ipAddress: request.ip,
success: reply.statusCode < 400,
});
});
};
}
Protected Endpoints:
| Endpoint | Resource Type |
|---|---|
GET /api/students/:id/transcript | transcript |
GET /api/students/:id/degree-progress | degree-progress |
GET /api/students/:id/enrollments | enrollment |
Sensitive Data Handling
- Credentials - Environment variables only, never in code
- Secrets - Not written to logs or error messages
- PII - Removed from all application logs
- Grades - Stored only in Firestore (encrypted at rest)
- Audit logs - Admin read-only, server-side write only
Performance Considerations
Caching Strategy
PBS Knowledge uses a multi-layer caching strategy:
- Redis API Cache - 24-48 hour TTL with explicit invalidation on data changes
- In-Memory Index - Publication existence checks for sync operations
- Static Assets - NGINX caching with long TTL
- Connection Pooling - Singleton TerminusDB client for connection reuse
GraphQL Optimization
- Minimal field queries - List views fetch only essential fields (60-80% smaller responses)
- Batch operations - Publication sync uses indexed lookups instead of N+1 queries
- Query builders - Separate minimal vs full query patterns
Background Processing
- BullMQ - Job queues for publication sync, email, data imports
- Rate limiting - Respects external API limits (OpenAlex)
- Cache invalidation - Automatic cache clearing after sync operations
For detailed implementation, see Performance & Caching.