Version: 1.0 Classification: Internal Engineering / AMD Hackathon Technical Submission Author: Principal Software Architecture Review Status: Production-Grade Reference Architecture
Gradelytics is a client-native, edge-AI-accelerated educational SaaS platform that automates rubric-based essay assessment for educators. The system is architected as a fully decoupled Single Page Application (SPA) with zero backend server dependency for its core grading workflow: all application state, credential management, and configuration persistence occur client-side, while computationally intensive natural-language inference is offloaded to a serverless edge inference layer — Fireworks AI, running on AMD Instinct GPU infrastructure.
This architectural posture yields three strategic advantages that anchor the platform's B2B SaaS value proposition:
- Zero-infrastructure operational cost — no application server, no database tier, and no session management layer to provision, patch, or scale.
- Data sovereignty by design — student essays and grading rubrics never transit or persist on a Gradelytics-owned server; the only network hop is teacher-browser → Fireworks AI inference endpoint.
- Sub-second time-to-first-byte and infinite horizontal scalability — the entire application is static output, served from a CDN-fronted Nginx edge container with no stateful compute to bottleneck concurrent tenants.
Gradelytics separates concerns into three strictly decoupled layers: a UI View Layer built on React 18 and Tailwind CSS 4, a Local Persistence Layer that repurposes the browser's localStorage as a lightweight client-side configuration state machine, and an Edge AI Inference Infrastructure Layer provided by Fireworks AI's OpenAI-compatible chat completions API, physically executing on AMD GPU silicon.
No proprietary backend, ORM, or relational database exists in this architecture. The browser is the runtime; localStorage is the persistence tier; Fireworks AI is the compute tier. This inversion — pushing state and orchestration to the edge/client while renting inference compute per-call — is what allows Gradelytics to operate as a stateless static bundle in production.
flowchart TB
subgraph Client["🖥️ Client Runtime — End User Browser"]
direction TB
subgraph ViewLayer["UI View Layer"]
React["React 18 Component Tree<br/>(App.tsx Root)"]
Tailwind["Tailwind CSS 4<br/>Utility-First Styling Engine"]
Recharts["Recharts Visualization Module<br/>(Grade Distribution / Analytics)"]
React --- Tailwind
React --- Recharts
end
subgraph PersistLayer["Local Persistence Layer — Config State Machine"]
LS[("Browser localStorage<br/>fireworks_api_key<br/>fireworks_model_id<br/>syllabus_data<br/>grading_history")]
end
ViewLayer <-->|"Read/Write Credentials<br/>& App Config (sync, no I/O latency)"| PersistLayer
end
subgraph Edge["☁️ Edge AI Inference Infrastructure Layer"]
direction TB
Gateway["Fireworks AI Inference Gateway<br/>OpenAI-Compatible Chat Completions API<br/>api.fireworks.ai/inference/v1"]
GPU["AMD Instinct GPU Fleet<br/>Serving Gemma / DeepSeek / Llama<br/>Class LLMs at Low-Latency Throughput"]
Gateway --> GPU
end
ViewLayer -->|"HTTPS POST — Bearer Token Auth<br/>Structured JSON Prompt Payload"| Gateway
Gateway -->|"Streamed / Completed JSON<br/>Grading Response"| ViewLayer
classDef clientStyle fill:#eff6ff,stroke:#2563eb,stroke-width:2px,color:#1e3a5f
classDef edgeStyle fill:#f0fdf4,stroke:#059669,stroke-width:2px,color:#064e3b
classDef persistStyle fill:#fef9c3,stroke:#ca8a04,stroke-width:2px,color:#713f12
class ViewLayer clientStyle
class Edge edgeStyle
class PersistLayer persistStyle
| Layer | Technology | Responsibility | Trust Boundary |
|---|---|---|---|
| UI View Layer | React 18 + Tailwind CSS 4 + Recharts + Lucide Icons | Declarative rendering, view-state routing, chart visualization, user interaction capture | Fully client-trusted; no secrets embedded at build time |
| Local Persistence Layer | Browser localStorage |
Durable client-side storage of API credentials (fireworks_api_key), selected inference model (fireworks_model_id), syllabus artifacts, and grading history — acting as a de facto embedded config state machine in lieu of a backend database |
Trusted only within the single-origin browser sandbox; never transmitted except as an Authorization: Bearer header directly to Fireworks AI |
| Edge AI Inference Infrastructure | Fireworks AI (AMD GPU-backed) | Stateless, on-demand LLM inference for rubric-grounded essay evaluation; returns structured JSON grading payloads | External, authenticated third-party boundary — the only network egress point in the entire application |
This three-tier separation means Gradelytics has no middle tier to compromise: there is no application server holding session tokens, no SQL injection surface, and no server-side credential store to breach. The attack surface collapses to browser-local storage hygiene and TLS-secured calls to a single, well-known third-party endpoint.
The component tree is rooted at App.tsx, which owns global view-routing state and renders a persistent Sidebar alongside a dynamically swapped View Controller. Below the View Controllers sit domain-specific presentational and container components, terminating in atomic UI cards (e.g., MetricCard) and shared utility modules that are imported horizontally across the tree rather than owned by any single view.
flowchart TD
App["📦 App.tsx<br/><i>Global App Wrapper</i><br/>• Active-view routing state (useState)<br/>• Global theme/design tokens<br/>• Toast / notification host"]
App --> Sidebar["🧭 Sidebar.tsx<br/><i>Sidebar Layout Component</i><br/>• Collapsible nav state<br/>• Connection Status Node<br/> (reads hasApiKey())<br/>• API Settings Modal trigger"]
Sidebar --> Modal["⚙️ ApiSettingsModal.tsx<br/>Fireworks API Key + Model ID form<br/>Writes to localStorage via<br/>saveApiKey() / saveModelId()"]
App --> ViewSwitch{"View Controller<br/>Router"}
ViewSwitch --> Dashboard["📊 DashboardView.tsx"]
ViewSwitch --> Syllabus["📐 SyllabusArchitectView.tsx"]
ViewSwitch --> VibeGrading["🎓 VibeGradingView.tsx<br/><i>(VibeGrading Simulator)</i>"]
ViewSwitch --> Analytics["📈 AnalyticsView.tsx<br/><i>(Analytics & History)</i>"]
Dashboard --> MetricCard["🔹 MetricCard.tsx<br/>Atomic KPI Card"]
Dashboard --> GradeChart["🔹 GradeDistributionChart.tsx<br/>Recharts Histogram/Bar"]
Analytics --> MetricCard
Analytics --> GradeChart
VibeGrading -.->|invokes| ApiClient
Dashboard -.->|reads| MockData
Analytics -.->|reads| MockData
subgraph SharedUtils["🔧 Shared Utility Modules — src/utils/"]
ApiClient["fireworksClient — utils/api.ts<br/>getApiKey / saveApiKey / hasApiKey<br/>getModelId / saveModelId<br/>callFireworksAI()"]
ExportUtils["utils/export.ts<br/>exportMarkdown() / exportPdf()<br/>jsPDF-based document generation"]
PrintUtils["printElement()<br/>html-to-image / html2canvas<br/>bridge for print & snapshot export"]
end
VibeGrading --> ExportUtils
Analytics --> ExportUtils
Dashboard --> PrintUtils
Analytics --> PrintUtils
classDef root fill:#1e293b,stroke:#0f172a,stroke-width:2px,color:#fff
classDef layout fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e3a5f
classDef view fill:#dcfce7,stroke:#16a34a,stroke-width:2px,color:#14532d
classDef atom fill:#fef3c7,stroke:#d97706,stroke-width:1.5px,color:#78350f
classDef util fill:#f3e8ff,stroke:#9333ea,stroke-width:2px,color:#581c87
class App root
class Sidebar,Modal layout
class Dashboard,Syllabus,VibeGrading,Analytics view
class MetricCard,GradeChart atom
class ApiClient,ExportUtils,PrintUtils util
App.tsx — Global App Wrapper. Owns the top-level activeView state enum (dashboard | syllabus | vibe-grading | analytics), instantiates global theme context, and hosts the toast/notification surface consumed by descendant views for asynchronous success/error feedback (e.g., "Grading complete", "API key saved").
Sidebar.tsx — Sidebar Layout Component. Manages its own isCollapsed boolean for responsive real-estate optimization, renders a live Connection Status Node that reactively reflects hasApiKey() from the shared fireworksClient, and exposes the trigger that mounts ApiSettingsModal.tsx as an overlay for credential configuration.
View Controllers. Each of DashboardView, SyllabusArchitectView, VibeGradingView, and AnalyticsView is a self-contained container component responsible for its own local UI state (loading/error/success), while delegating all network I/O to the shared fireworksClient and all document generation to utils/export.ts. This enforces a strict single-responsibility boundary: views orchestrate; utilities execute cross-cutting concerns.
Atomic UI Cards. MetricCard.tsx and GradeDistributionChart.tsx are pure, stateless, prop-driven presentational components reused across DashboardView and AnalyticsView, ensuring visual and behavioral consistency for KPI display and grade distribution rendering without duplicated logic.
Shared Utilities. fireworksClient (utils/api.ts) centralizes all outbound HTTP to the inference gateway and all localStorage credential access behind a narrow function-level API (getApiKey, saveApiKey, hasApiKey, getModelId, saveModelId, callFireworksAI) — no component touches window.localStorage or fetch directly. printElement and exportMarkdown/PDF export (utils/export.ts, built on jsPDF and html-to-image/html2canvas) provide document-generation and print-snapshot capabilities consumed identically by both VibeGradingView and AnalyticsView.
The centerpiece of Gradelytics' functional value is the VibeGrading Simulator Execution Pipeline — a fully client-orchestrated sequence that transforms a teacher-authored rubric and a raw student essay into a structured, AI-generated grading result, rendered as reactive dashboard state.
The pipeline is intentionally stateless and idempotent per invocation: no grading request depends on server-side session context, and every request carries its full instruction payload (rubric + essay + output-format constraint) in a single HTTP call.
sequenceDiagram
autonumber
actor Teacher as Teacher (User)
participant UI as VibeGradingView.tsx
participant LS as localStorage<br/>(Config State Machine)
participant Client as fireworksClient<br/>(utils/api.ts)
participant Edge as Fireworks AI<br/>Edge Gateway (AMD GPU)
participant State as Global Dashboard State<br/>(React state / Recharts)
Teacher->>UI: Enter Rubric Criteria (JSON metrics array)
Teacher->>UI: Paste raw Student Essay (string)
Teacher->>UI: Click "Run VibeGrading"
activate UI
UI->>UI: Guard: validate non-empty rubric + essay
UI->>UI: Flip state → isGrading = true<br/>Render animated "Analyzing via Gemma 4 / DeepSeek…" pulse
UI->>LS: getApiKey() / getModelId()
activate LS
LS-->>UI: fireworks_api_key, fireworks_model_id
deactivate LS
alt No API key configured
UI-->>Teacher: Toast error — "Configure Fireworks API key"
UI->>UI: isGrading = false
else Key present
UI->>Client: callFireworksAI(messages, options)
activate Client
Client->>Client: Serialize rubric[] + essay into<br/>structured Prompt Schema<br/>(system: "return JSON only" + schema hint)
Client->>Edge: HTTPS POST /inference/v1/chat/completions<br/>Authorization: Bearer {apiKey}<br/>body: { model, messages, temperature, max_tokens }
activate Edge
Note over Edge: AMD GPU-accelerated LLM inference<br/>(Gemma / DeepSeek / Llama-class model)<br/>generates rubric-grounded JSON verdict
Edge-->>Client: 200 OK — choices[0].message.content<br/>(JSON string: scores, feedback, rationale)
deactivate Edge
Client->>Client: extractJsonFromResponse()<br/>strip ```json fences, JSON.parse()
Client-->>UI: Deterministic GradingResult object
deactivate Client
UI->>State: setGradingResult(parsedResult)
activate State
State->>State: Recompute derived metrics<br/>(overall score, per-criterion breakdown)
State-->>UI: Trigger re-render
deactivate State
UI->>UI: isGrading = false
UI-->>Teacher: Render MetricCard grid +<br/>GradeDistributionChart (Recharts) +<br/>Typography feedback layout
end
deactivate UI
-
User Inputs. The teacher populates a
RubricCriterion[]array (each entry:{ label, weight, description }) and supplies a raw essaystringvia a controlled textarea. Submission is gated behind a client-side guard requiring both fields non-empty before the pipeline may trigger. -
Guard & State Trigger. On trigger, the view synchronously flips
isGradingtotrue, which mounts an animated pulse/skeleton state communicating active inference (e.g., "Analyzing via Gemma 4 / DeepSeek…"). Concurrently, the pipeline callsgetApiKey()andgetModelId()againstlocalStorage— a synchronous, zero-latency read with no network round-trip, reinforcing the "config state machine" role of the persistence layer. -
API Payload Construction.
fireworksClient.callFireworksAI()serializes the rubric metrics and essay text into a structured Prompt Schema: a system instruction constraining the model to emit valid JSON matching a fixed grading schema (per-criterion scores, weighted total, qualitative feedback), paired with a user message carrying the rubric and essay content. This schema-forcing approach is what makes the downstream parsing step deterministic rather than probabilistic. -
Serverless Edge Gateway. The client issues an asynchronous
fetch()POSTtohttps://api.fireworks.ai/inference/v1/chat/completions, authenticated via aBearertoken drawn fromlocalStorage. This request is routed to Fireworks AI's inference fleet, executing on AMD Instinct GPU infrastructure, which serves the configured model (Gemma, DeepSeek, or Llama-class, selectable viafireworks_model_id). -
Deterministic Response Parsing. On receipt,
extractJsonFromResponse()strips any Markdown code-fence wrapping (json …) the model may emit, andJSON.parse()s the remainder into a strongly-typedGradingResult. This object is committed to the view's local React state, which cascades into re-renders ofMetricCardKPI tiles, theGradeDistributionChartRecharts visualization, and the qualitative feedback typography block — completing the closed loop from raw text input to structured, visualized pedagogical insight, withisGradingreset tofalseto restore the interactive UI.
Because Gradelytics' entire runtime is a client-executed SPA with no server-side rendering and no backend API of its own, the production deployment artifact is build-time-compiled static assets (HTML, hashed JS/CSS bundles, and public assets) — nothing more. This has direct compliance and cost implications:
- No PII/FERPA-scoped data ever touches Gradelytics infrastructure. Student essays and grading rubrics flow directly from the teacher's browser to Fireworks AI over TLS; the hosting layer serves only immutable, non-personalized static files and never sees request bodies.
- The container is compute-idle by design. Nginx's role is exclusively to serve pre-built files and handle SPA client-side routing fallback — it performs no template rendering, no session handling, and no database queries, which minimizes the compliance surface for SOC 2 / FERPA-adjacent review.
- Horizontal scaling is trivial and stateless. Because every container instance is byte-identical and holds no session affinity, the runtime can be replicated behind any load balancer or CDN edge network without sticky sessions.
The deployment model follows a two-stage build/runtime separation: a transient Node.js Alpine builder compiles the Vite/React/TypeScript source into a static dist/ bundle, which is then copied — and only the compiled output, never node_modules or source — into a minimal Nginx Alpine image for production serving. This keeps the shipped image free of build toolchains, npm registries, and dev dependencies, reducing both image size and attack surface.
flowchart LR
subgraph Stage1["Stage 1 — Build (ephemeral, discarded)"]
direction TB
Node["node:20-alpine<br/>npm ci<br/>vite build"]
Src["Source: src/, index.html,<br/>vite.config.ts, tsconfig.json"]
Dist["Output: /app/dist<br/>(hashed static bundle)"]
Src --> Node --> Dist
end
subgraph Stage2["Stage 2 — Production Delivery (shipped image)"]
direction TB
Nginx["nginx:alpine<br/>~7MB base image"]
Conf["Custom nginx.conf<br/>SPA fallback: try_files → index.html<br/>gzip, cache-control headers"]
Serve["Serves static assets on :80<br/>No compute, no state, no DB"]
Nginx --> Conf --> Serve
end
Dist -->|"COPY --from=builder<br/>/app/dist → /usr/share/nginx/html"| Nginx
classDef build fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#78350f
classDef prod fill:#dcfce7,stroke:#16a34a,stroke-width:2px,color:#14532d
class Stage1 build
class Stage2 prod
# ─────────────────────────────────────────────────────────────
# Stage 1: Build — compiles the Vite/React/TypeScript SPA
# ─────────────────────────────────────────────────────────────
FROM node:20-alpine AS builder
WORKDIR /app
# Leverage Docker layer caching: install deps before copying source
COPY package.json package-lock.json ./
RUN npm ci --no-audit --no-fund
# Copy source and compile production bundle
COPY . .
RUN npm run build
# → outputs static assets to /app/dist
# ─────────────────────────────────────────────────────────────
# Stage 2: Production Delivery — ultra-lightweight Nginx runtime
# ─────────────────────────────────────────────────────────────
FROM nginx:1.27-alpine AS production
# Remove default Nginx welcome config; replace with SPA-safe routing
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Copy only the compiled static bundle — no source, no node_modules
COPY --from=builder /app/dist /usr/share/nginx/html
# Run as non-root for defense-in-depth
RUN addgroup -g 1001 -S appgroup \
&& adduser -S appuser -u 1001 -G appgroup \
&& chown -R appuser:appgroup /usr/share/nginx/html \
&& chown -R appuser:appgroup /var/cache/nginx /var/run \
&& touch /var/run/nginx.pid \
&& chown appuser:appgroup /var/run/nginx.pid
USER appuser
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
CMD wget --quiet --tries=1 --spider http://localhost:80/ || exit 1
CMD ["nginx", "-g", "daemon off;"]Companion nginx.conf — required to safely handle client-side routing for the React Router-style view switching used by App.tsx, ensuring deep-linked or refreshed routes resolve to index.html rather than a 404:
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Gzip static compression for JS/CSS bundle delivery
gzip on;
gzip_types text/plain text/css application/javascript application/json image/svg+xml;
gzip_min_length 1024;
# Aggressive caching for hashed, immutable Vite build assets
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# SPA fallback — critical for client-side view routing
location / {
try_files $uri $uri/ /index.html;
add_header Cache-Control "no-cache";
}
# Security headers — defense-in-depth for a public-facing static app
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
}- Image footprint. The final production image inherits only from
nginx:alpine(~7 MB base), plus the compiled static bundle — typically well under 25 MB total, enabling fast pulls and rapid autoscaling. - No secrets baked into the image. Because the Fireworks AI API key is supplied per-teacher at runtime via the
ApiSettingsModaland persisted client-side inlocalStorage, the Docker image itself carries zero embedded credentials — a meaningful compliance win, since the container registry never becomes a secrets-leak vector. - CI/CD alignment. This blueprint is directly compatible with any container-native deployment target (AMD-accelerated Kubernetes clusters, ECS/Fargate, Cloud Run, or a simple
docker run), since the runtime stage has no external service dependencies beyond outbound HTTPS toapi.fireworks.ai.
| Concern | Design Decision | Rationale |
|---|---|---|
| State management | localStorage as config state machine (no Redux/backend DB) |
Eliminates infra tier; instant reads; scoped to browser origin |
| AI compute | Fireworks AI on AMD GPU infrastructure, called client-side | Pay-per-inference economics; no self-hosted model-serving burden |
| Data residency | Essay/rubric data never touches Gradelytics servers | Simplifies FERPA/PII compliance posture by design, not policy |
| Deployment unit | Static bundle behind multi-stage Docker + Nginx Alpine | Stateless, horizontally scalable, minimal attack surface |
| Component boundary | View Controllers delegate I/O to shared utilities (fireworksClient, export.ts) |
Single-responsibility, testable, no duplicated network/export logic |
This architecture positions Gradelytics as a lean, defensible reference implementation of an edge-AI-native educational SaaS product: every architectural choice — from the localStorage-as-state-machine pattern to the AMD GPU-backed inference call to the two-stage Nginx delivery model — is optimized for near-zero operational overhead while preserving a scalable, security-conscious foundation suitable for B2B expansion.