← Back

Web App · Open Source

AkademData

Status

Live at MVNU

Stack

Next.js 15PrismaExcelJSSQLiteChart.js

[ THE THESIS ]

From open-source core to Momentum at MVNU

Universities are drowning in Excel Hell when it comes to federal reporting and compliance. I engineered AkademData as an open-source data architecture for parsing, standardizing, and auditing messy institutional files. I deployed that core to build Momentum at Mount Vernon Nazarene University (MVNU)—a live reporting engine that turns multi-day manual workflows into instant dashboards with data provenance, audit checks, and inspectable open-source foundations.

LIVE · Momentum at MVNU · open-source core v1.0.2

Technical proof
129
programs parsed
zero errors
2,100+
FTE records
unified in one DB
2 min
from 2 days
stats-day report time
Audit
checks built in
pre-submission validation

SECTION 01DESIGN DECISIONS

AkademData is the core. Momentum is the MVNU deployment.

I built AkademData as an open-source higher-ed data architecture, then deployed that core to build Momentum for Mount Vernon Nazarene University. The system takes messy stats-day Excel files, standardizes them into an auditable data model, and gives MVNU a working reporting engine without starting from a full SIS replacement.

01

Open-source first — then commercial

akademdata-open-source@1.0.2 is the inspectable core behind Momentum. A school can review the parsing logic, schema decisions, and audit flow before trusting the deployment.

02

Meet the data where it lives

Schools already have stats-day .xlsx files. The ingest path is a drag-and-drop upload; the schema is inferred from a runtime JSON config. Zero SIS work.

03

Star-schema, not wide tables

Student is the dimension, Enrollment is the fact table. One student → N enrollments (term × program). The right shape for analytics grouping without denormalised duplication.

04

Provenance before persuasion

Every metric is designed to trace back to the raw file, the rule applied, and the records underneath it. Momentum is not just a dashboard — it is an audit trail.

SECTION 02INGESTION PIPELINE

Excel → SekanData Mapper → Prisma → server-rendered analytics

UI · Next.js 15 · force-dynamic server rendering

Upload

drag-drop .xlsx

Stats

students / enrollments / programs

Search

URL searchParams

Export

CSV / xlsx out

SekanData Mapper · lib/parser.ts · the engineering core

ExcelJS streaming parse

  • ▹ row-by-row via ws.eachRow
  • ▹ memory-safe on institution-scale files
  • ▹ typed getString / getInt / getFloat

Graceful row failure

  • ▹ missing studentId → warn + skip
  • ▹ merged headers ignored
  • ▹ hand-entered notes ignored

Multi-program data normalization

One raw Excel row → Student.upsert (dedup by studentId) + Enrollment.create (fact row — one per term/program). No duplicate students, no broken joins.

Prisma ORM · SQLite (dev) / Postgres (prod)

Student (dimension)

  • ▹ studentId @unique
  • ▹ name · age · gender · state
  • ▹ createdAt

Enrollment (fact)

  • ▹ studentId → Student
  • ▹ term · program · credits · status
  • ▹ 1 student ⇒ N rows

SECTION 03MOMENTUM AT MVNU

The delta from manual reporting to auditable dashboards

Before · Excel hell

After · Momentum

Enrollment report

~2 days

~2 minutes

Programs parsed

hand-managed

129 · zero errors

FTE records

fragmented across files

2,100+ unified

Growth tracking

retrospective, manual

live 24% visibility

Reporting validation

manual cross-checks

historical anomalies reconciled

Metric provenance

numbers without receipts

source file + rule + records

IR staff time / month

20–40 hours

reclaimed for analysis

Labor cost exposure

$15k–$40k / year

reduced via automation

HLC 2029 readiness

at risk

on track

SECTION 04ADAPT PLAYBOOK

Five steps to deploy to another school

docs/ADAPT.md — a few hours, not a six-month integration project.

01

Edit prisma/schema.prisma

Add or remove fields to match your data model. Generate the migration.

02

Update data/colMappings.json

Point Excel columns (1-based indices) to your schema fields. Runtime config, no rebuild.

03

Tweak lib/parser.ts

If you've added fields, extend the data object. Everything else (streaming, upsert, graceful skip) stays the same.

04

Customise the dashboard

Update getDashboard() and getChartData() in lib/queries.ts. The chart components auto-pick up the new shape.

05

Swap branding

Replace public/logo.svg, adjust tailwind.config.js tokens. Deploy.

SECTION 05ENGINEERING HIGHLIGHTS

What this codebase proves

PROVENANCE

Hover-to-prove-it metrics

Every rendered number is designed to carry its receipts: source file, applied rule, matching status, and the underlying student records. Leadership sees the metric; data teams can prove it.

audit-ready by default

AUDIT

Automated reconciliation before final review

Momentum cross-references live database calculations against historical reporting artifacts, surfacing anomalies early so the data team can reconcile edge cases before final review.

pre-submission validation

SCALE

Tenant rules live outside the core engine

Institutional business rules — program exclusions, cohort definitions, column mappings, and reporting assumptions — are configuration, not rewrites. That is what makes the open-source core reusable beyond MVNU.

built for other campuses

SECTION 06CODE PROOF

The mapper that turns any stats-day .xlsx into a clean relational DB

Verbatim from the NPM package.

lib/parser.ts

typescript

// lib/parser.ts — excerpt · the SekanData Mapper
const COLS = getColMapping();        // JSON config merged over DEFAULT_COLS

ws.eachRow({ includeEmpty: false }, async (row, n) => {
  if (n === 1) return;                        // header row
  const studentId = row.getCell(COLS.studentId).value?.toString();
  if (!studentId) {                            // graceful row skip
    console.warn(`Row ${n}: missing studentId — skipping`);
    return;
  }

  const student = {
    studentId,
    name:   getString(row, COLS.name),
    age:    getInt(row,    COLS.age),
    gender: getString(row, COLS.gender),
    state:  getString(row, COLS.state),
  };

  const enrollment = {
    studentId,
    term:    getString(row, COLS.term),
    program: getString(row, COLS.program),
    credits: getFloat(row,  COLS.credits),
    status:  getString(row, COLS.status),
  };

  // 1 student → 1 dimension row (dedup by unique studentId)
  await prisma.student.upsert({
    where:  { studentId },
    update: student,
    create: student,
  });

  // 1 student → N fact rows (term × program)
  if (enrollment.term && enrollment.program) {
    await prisma.enrollment.create({ data: enrollment });
  }
});