Skip to main content

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
  1. User clicks login
  2. Redirected to Dartmouth SSO
  3. User authenticates
  4. SAML assertion returned
  5. Backend validates and creates session
  6. JWT issued for API access

Authorization

Permissions checked at multiple levels:

  1. Route middleware (authentication)
  2. Service layer (authorization)
  3. Database layer (field-level)

Job Processing

BullMQ Architecture

Event/Schedule → Job Queue (Redis) → Worker → Result

Queue Types

QueuePurpose
emailSend transactional emails
syncData synchronization
publicationPublication import
backupDatabase 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

  1. NGINX - Rate limiting, headers
  2. Backend - Authentication, validation
  3. Services - Authorization
  4. 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?

ConcernSolution
Grades need encryption at restFirestore provides automatic AES-256 encryption
FERPA audit trail requiredAll grade access logged to audit_logs collection
Compliance certificationsFirebase has SOC 2, ISO 27001
Data minimizationSensitive 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:

EndpointResource Type
GET /api/students/:id/transcripttranscript
GET /api/students/:id/degree-progressdegree-progress
GET /api/students/:id/enrollmentsenrollment

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.