The post-payment command center for live tech education at scale.
Product Vision · V1 Scope · Architecture · Database · Security · Deploy · Roadmap
- Product Vision
- Platform Overview
- Current V1 Launch Scope
- Key Features
- Architecture Deep Dive
- Database Design
- Authentication & Security
- Student Journey
- Batch Management System
- Zoom Integration
- Roadmap Engine
- Role-Based Access Control
- API Documentation
- Folder Structure
- Local Development Setup
- Environment Variables
- Deployment Guide
- Performance Optimizations
- Operational Philosophy
- Scalability Vision
- Future Roadmap
- Testing Strategy
- Monitoring & Observability
- Contributor Guide
- Lessons Learned
- Screenshots
- License
- Contact
MicroDegree runs live cohort-based programs in Cloud Computing and Gen AI for Indian learners. After payment, operations historically fractured across spreadsheets, WhatsApp groups, ad-hoc Zoom links, and disconnected tools. Students asked the same question repeatedly: “Where is my class link?” Ops burned hours on manual coordination instead of delivery quality.
MicroDegree Hub is the Student Experience Layer + Internal Operations Console — a single domain (hub.microdegree.work) that owns everything after money changes hands:
| Problem (before) | Hub response (after) |
|---|---|
| Fragmented onboarding | Stateful enrollment lifecycle with audit trail |
| Lost Zoom links | Session-aware join with join windows + S2S Zoom URLs |
| No cohort visibility | Batch assignments, attendance, ops dashboards |
| Roadmap opacity | Plan-ordered course roadmap with tentative future batches |
| No accountability | Immutable audit_logs on every mutation |
| Role chaos | Server-side RBAC via can() — not UI-only hiding |
Hub is not an LMS (Thinkific remains), not a CRM (LeadSquared pre-sale), not a placement portal (portal.microdegree.work), not a mock-test platform (practice.microdegree.work). We deep-link to sister products and own cohort operations.
Correctness and operability beat feature breadth.
A monolithic Next.js application with a strict service layer, Postgres state machines, and defense-in-depth auth ships faster and fails more predictably than premature microservices.
flowchart TB
subgraph Public["hub.microdegree.work"]
STU["/app — Student Portal"]
OPS["/admin/ops — Operations"]
AC["/admin/ac — Academic Counselor"]
SALES["/sales — Sales AC"]
AUTH["/auth — OTP + Google SSO"]
API["/api — Cron + Zoom join"]
end
subgraph Data["Supabase ap-south-1"]
PG[(PostgreSQL)]
SA[Supabase Auth]
STORE[Storage - future]
end
subgraph External["Integrations"]
ZOOM[Zoom S2S OAuth]
RESEND[Resend SMTP]
THINK[Thinkific deep links]
RAZOR[Razorpay - future]
end
STU --> AUTH
OPS --> AUTH
STU --> PG
OPS --> PG
AUTH --> SA
API --> ZOOM
API --> RESEND
STU --> THINK
| Portal | Route prefix | Primary users | Maturity |
|---|---|---|---|
| Student | /app/* |
Enrolled learners | V1 — production |
| Operations | /admin/ops/* |
Ops, PM, support | V1 — production |
| Academic Counselor | /admin/ac/* |
AC staff | Built; de-emphasized in V1 pilot |
| Sales | /sales/* |
Sales / AC submitters | Built; out of V1 pilot scope |
| Staff auth | /admin/login, /auth/* |
All staff | V1 — production |
- Identity mirrors Supabase Auth into
users+ role-specific profiles. - Enrollment is the root aggregate — state machine drives lifecycle (
ACTIVE,PAUSED,EXPIRED, …). - Batch assignments connect enrollments to live cohorts (
batch_assignments). - Sessions schedule Zoom-backed classes; students join via gated API route.
- Notifications queue transactional email; cron workers drain the queue.
- Audit captures before/after snapshots on every mutation.
V1 is an intentionally narrow internal launch — “Student Learning Hub Beta” — not full commercial GTM.
| In V1 | Deferred (V1.5+) |
|---|---|
| Preloaded existing AWS / running cohort students | New payer sales funnel as primary path |
| Manual CSV import + batch calendar CSV | Automated CRM → Hub lead sync |
| OTP login + dashboard + Zoom join | Full counselor-led onboarding UX |
| Roadmap with tentative future batches | Payment automation / Razorpay webhooks in critical path |
| Ops batch/session management | Tally AC webhook |
| Tickets + SLA crons (supporting) | WhatsApp BSP (WATI) automation |
Reliability → Clarity → Throughput → Features
Why limit scope?
- Operational truth: Running cohorts already exist; software must not block Monday class.
- Risk reduction: Fewer moving parts = fewer failure modes during first live week.
- Data quality: Manual preload forces clean batch ↔ student mapping before automation.
- Feedback loop: Validate join flow, roadmap clarity, and ops console speed before opening sales firehose.
Set HUB_PILOT_LAUNCH=true to de-emphasize sales/AC navigation and optimize ops paths for bulk legacy import (/admin/ops/students/import).
| Feature | Business value | Technical implementation |
|---|---|---|
| OTP / magic-link login | Zero password friction; inbox-verified identity | Supabase signInWithOtp → /auth/confirm → provisionStudentUserFromInvite |
| Home dashboard | “What do I do next?” in one screen | RSC bundle: enrollment + next session + validity (getStudentEnrollmentBundle) |
Classes (/app/batches) |
Current cohort visibility + attendance | getMyBatches, batch_assignments + attendance_records |
| Zoom join | Eliminates “link in WhatsApp” failure mode | POST /api/app/zoom/join — S2S OAuth, join window, payment gate |
Roadmap (/app/roadmap) |
Retention via future learning visibility | plan_courses order + tentative batch start labels |
Plan & pause (/app/plan) |
Self-serve pause ≤30 days | requestEnrollmentPause with auto-approve rules |
Batch transfer (/app/batches) |
Self-serve cohort change request | requestBatchTransfer → ops approval |
| Calendar | Schedule clarity | Sessions across assigned batches |
Tickets (/app/tickets) |
Structured support vs DMs | Ticket state machine + SLA cron |
Workshops (/app/workshops) |
Supplementary live events | RSVP + eligibility rules |
| Profile / intake | Data completeness | intake_forms, profile mutations |
| Feature | Business value | Technical implementation |
|---|---|---|
| Student search & detail | Single pane of glass for support | getStudentOpsDetail composite query |
| CSV import (pilot) | Bulk legacy student load | importLegacyStudentsFromCsv + synthetic verified AC |
| Batch lifecycle | Forming → active → cancelled | startBatch, cancelBatch, auto-transfer |
| Session Zoom IDs | Ops controls join without deploy | sessions.zoom_meeting_id per row |
| Pipeline / onboarding | Task checklist per enrollment | onboarding_tasks |
| Tickets queue | SLA visibility | listTicketsQueue, processSlaBreaches |
| Enrollment lifecycle panel | Pause / transfer / upgrade | Pack 008 services + audit |
| Workshops admin | Create / manage events | createWorkshop, ops CRUD |
| Portal | Purpose |
|---|---|
Sales /sales |
Native AC form submission → ops verification queue |
AC /admin/ac |
Counselor verification + student detail |
flowchart LR
subgraph Client
Browser[Browser]
end
subgraph Next["Next.js 16 App Router"]
RSC[React Server Components]
SA[Server Actions]
RH[Route Handlers /api]
MW[Middleware + guards]
end
subgraph Domain["Domain Layer src/lib/services"]
SVC[Services]
SM[State machines]
AUD[Audit emitter]
end
subgraph Data
DRZ[Drizzle ORM]
PG[(Postgres)]
end
Browser --> RSC
Browser --> SA
RSC --> SVC
SA --> SVC
RH --> SVC
SVC --> DRZ --> PG
SVC --> AUD
- No business logic in routes — Server Actions orchestrate;
lib/services/*decides. - No raw SQL state updates — enrollment/batch/ticket transitions go through state-machine functions.
- Every mutation audits —
audit_logswithbefore_snapshot/after_snapshot. - Authorization server-side —
assertCan(ctx, action, resource); UI hiding is courtesy only. - Money in integer paisa —
amount_paisa; format at display layer. - Timestamps UTC — display in IST via
date-fns-tz. - Soft delete —
deleted_at IS NULLdefault scopes.
| Layer | Choice | Rationale |
|---|---|---|
| Framework | Next.js App Router | Collocated UI + API + auth; RSC reduces client JS |
| Language | TypeScript strict | Contract safety across services and forms |
| Styling | Tailwind 4 + shadcn/ui | Consistent design system, accessible primitives |
| Forms | React Hook Form + Zod | Shared client/server validation shapes |
| State | Server-first | No global client store for domain data; mutations revalidate paths |
Rendering strategy
- RSC for dashboards, lists, detail pages — data fetched on server with request-scoped auth context.
- Client components only for interactivity: OTP form, join button, filters, toasts (
sonner). revalidatePath/ tags after Server Actions — freshness without full SPA refetch.
There is no separate FastAPI/Node microservice. The “backend” is:
| Surface | Use case |
|---|---|
| Server Actions | Mutations from staff/student UI |
| Route Handlers | Cron endpoints, Zoom join API, health check |
| Service layer | Transactions, rules, audit, events |
// Canonical mutation flow (simplified)
export async function requestEnrollmentPause(input: { ctx, data }) {
return getDb().transaction(async (tx) => {
const enrollment = await loadEnrollment(tx, data.enrollmentId);
await assertCan(input.ctx, 'enrollment_pause.request', enrollment);
// business rules...
const pause = await tx.insert(enrollmentPauses).values({...}).returning();
await logAudit(tx, input.ctx, { action: 'pause.requested', ... });
return pause;
});
}sequenceDiagram
participant S as Student
participant H as Hub /auth
participant SB as Supabase Auth
participant DB as Postgres
S->>H: Email on /auth/login
H->>SB: signInWithOtp
SB-->>S: Email OTP / magic link
S->>H: /auth/confirm?flow=student
H->>SB: exchangeCodeForSession
H->>DB: provisionStudentUserFromInvite
H-->>S: Session cookie → /app
Staff flow uses Google OAuth → /auth/callback → provisionStaffUserOnFirstLogin with allowlist (allowed_staff_emails + env lists).
| Strategy | Application |
|---|---|
| Request-scoped DB reads | No cross-request cache for auth-bound data |
| Postgres as SoT | No Redis in V1 — simplicity |
| Cron idempotency | Notification sends use idempotency keys |
| Optimistic UI | Limited; prefer server revalidation for financial/state data |
- Connection pooling via Supabase pooler (port
6543). - Indexed foreign keys on enrollment, batch, ticket queues.
- BRIN on
audit_logs.created_atfor time-range queries. - Horizontal scale = stateless Next.js replicas behind load balancer; cron via external scheduler.
- Future: read replicas, queue workers (Inngest/BullMQ), event bus — see §20.
erDiagram
users ||--o| student_profiles : has
users ||--o| staff_profiles : has
student_profiles ||--o{ enrollments : owns
enrollments ||--o{ batch_assignments : has
batches ||--o{ sessions : contains
batches ||--o{ batch_assignments : receives
enrollments ||--o{ enrollment_pauses : may
enrollments ||--o{ batch_transfers : may
enrollments ||--o{ tickets : raises
users ||--o{ audit_logs : actor
| Table | Purpose |
|---|---|
users |
Mirrors auth.users; role + identity |
student_profiles |
MD-NNNN code, PII extensions |
staff_profiles |
Trainer/ops metadata |
allowed_staff_emails |
Google SSO allowlist |
student_code_counter |
Atomic MD-1001 generation |
plans / courses / plan_courses |
Catalog & roadmap ordering |
ac_forms |
Admission completion record |
enrollments |
Root lifecycle aggregate |
payments |
Verified money in paisa |
intake_forms |
Student intake submission |
onboarding_tasks |
Checklist per enrollment |
batches |
Cohort container |
sessions |
Scheduled class instances |
batch_assignments |
Enrollment ↔ batch ↔ course |
attendance_records |
Per session per assignment |
batch_transfers |
Cohort change workflow |
enrollment_pauses |
Plan pause workflow |
tickets / ticket_messages |
Support |
notifications |
Email queue |
audit_logs |
Immutable mutation trail |
workshops / workshop_rsvps |
Supplementary events |
PAYMENT_VERIFIED → PRE_ONBOARDING → BATCH_PENDING → BATCH_ASSIGNED → ACTIVE
↓
PAUSED / EXPIRED / COMPLETED
Transitions only via transitionEnrollment() and related service triggers — never ad-hoc UPDATE state.
-- Enrollments carry plan validity and explicit state
CREATE TYPE enrollment_state AS ENUM (
'PAYMENT_VERIFIED', 'PRE_ONBOARDING', 'BATCH_PENDING',
'BATCH_ASSIGNED', 'ACTIVE', 'PAUSED', 'SUSPENDED',
'COMPLETED', 'EXPIRED', 'REFUNDED', 'CANCELLED'
);
CREATE TABLE enrollments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
student_profile_id UUID NOT NULL REFERENCES student_profiles(id),
plan_id UUID NOT NULL REFERENCES plans(id),
ac_form_id UUID NOT NULL REFERENCES ac_forms(id),
state enrollment_state NOT NULL,
plan_validity_end_date DATE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted_at TIMESTAMPTZ
);
-- One active assignment per enrollment+course (partial unique index)
CREATE TABLE batch_assignments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
enrollment_id UUID NOT NULL REFERENCES enrollments(id),
batch_id UUID NOT NULL REFERENCES batches(id),
course_id UUID NOT NULL REFERENCES courses(id),
status assignment_status NOT NULL,
transfer_count INT NOT NULL DEFAULT 0,
assigned_at TIMESTAMPTZ NOT NULL DEFAULT now()
);- FK columns indexed by default usage paths (
enrollment_id,batch_id,student_profile_id). - Ticket queue: composite on
(status, assigned_to_user_id). - Audit: BRIN on
created_atfor time-slice queries. - Partial unique indexes enforce one active assignment per course per enrollment.
13 versioned SQL packs (0001–0013) applied via:
pnpm db:apply:allLedger table _hub_schema_migrations makes re-apply safe on existing environments.
| Mechanism | Detail |
|---|---|
| Primary | Email OTP + optional magic link (/auth/login, /auth/confirm) |
| Session | Supabase SSR cookies via @supabase/ssr |
| Provisioning | users + student_profiles created on first successful auth |
| Invite (pilot) | inviteUserByEmail on CSV import (optional) |
| Mechanism | Detail |
|---|---|
| Primary | Google OAuth |
| Allowlist | allowed_staff_emails table + STAFF_OPS_ALLOWLIST / STAFF_SALES_ALLOWLIST env |
| First login | provisionStaffUserOnFirstLogin assigns role from allowlist |
| Denied | Non-allowlisted Google accounts → /auth/denied |
Authorization is action-based, not route-based:
await assertCan(ctx, 'batch_transfer.request', enrollment);Policies live in src/lib/auth/can.ts — 30+ actions (enrollment.read_own, ticket.manage, batch.create, …).
- Middleware validates session for
/app/*and/admin/*. - Server layouts call
getAuthContextOrThrow()— fail closed. - Service layer re-checks permissions on every mutation.
| Layer | Mechanism |
|---|---|
| Application | assertCan, service-level ownership checks |
| Database | RLS policies on student-facing tables (Pack 007+) |
| Network | Cron routes require Authorization: Bearer CRON_SECRET |
| Secrets | SUPABASE_SERVICE_ROLE_KEY server-only; never NEXT_PUBLIC_* |
- Mutations →
audit_logswith snapshots (restricted table). - PII reads by non-owning roles can emit audit rows per policy.
- No PII in unstructured
actionstrings.
| Class | Examples | Exposure |
|---|---|---|
| Public | NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SITE_URL |
Client bundle |
| Server-only | DATABASE_URL, SUPABASE_SERVICE_ROLE_KEY, CRON_SECRET, ZOOM_*, RESEND_API_KEY |
Node runtime only |
Run pnpm verify:prod-env before every deploy.
journey
title Student Journey (V1 Pilot)
section Onboarding
Receive WhatsApp/email with Hub link: 5: student
OTP login: 4: student
Land on dashboard: 5: student
section Weekly loop
View next session: 5: student
Join Zoom (join window): 5: student
Check roadmap for future batches: 4: student
section Exceptions
Request pause on /app/plan: 3: student
Request batch transfer: 3: student
Open support ticket: 4: student
| Step | Student experience | System behavior |
|---|---|---|
| 1 | Receives login link (ops import or manual) | Auth user + enrollment pre-created |
| 2 | /auth/login → OTP |
Supabase email via Resend SMTP |
| 3 | /app dashboard |
Next session, validity banner, join CTA |
| 4 | Join class | ZoomJoinButton → /api/app/zoom/join |
| 5 | /app/roadmap |
Completed / in-progress / upcoming courses |
| 6 | Optional pause/transfer/ticket | Server Actions → ops approval queues |
planning → forming → active → completed
↘ cancelled (may auto-transfer students)
| Status | Meaning |
|---|---|
planning |
Future placeholder cohort (roadmap labels) |
forming |
Accepting assignments |
active |
Live — sessions run, join enabled |
cancelled |
Ops-initiated with reason; transfer engine fires |
batch_assignmentslinksenrollment_id+course_id+batch_id.- Status:
assigned→active→completed/transferred_out. - CSV import can bulk-assign via
student_emailscolumn on batch calendar upload.
Future batches in planning/forming show “Starts YYYY-MM-DD (tentative)” on student roadmap — retention without over-committing dates.
| Component | Role |
|---|---|
lib/integrations/zoom/* |
S2S OAuth token cache, meeting API |
sessions.zoom_meeting_id |
Per-class meeting ID (ops-set) |
batches.zoom_join_url |
Optional fallback |
POST /api/app/zoom/join |
AuthZ + join window + registrant display name |
- Student authenticated with active assignment.
- Enrollment not blocked by payment verification (pilot: legacy AC bypass).
- Session within join window (scheduled start ± policy).
- Meeting ID present on session or batch.
- Zoom webhooks → automatic attendance sync.
- Recording link automation.
| Source | Drives |
|---|---|
plan_courses.display_order |
Vertical roadmap sequence |
batch_assignments + courses |
In-progress vs completed |
Open batches (planning/forming) |
Tentative upcoming labels |
Roadmap reduces “what happens after AWS?” anxiety — critical for multi-month programs and upsell continuity without sales calls.
Self-serve transfer on /app/batches creates batch_transfers row → ops approves → optional fee → payment confirm → assignment swap.
| Role | Portal | V1 usage |
|---|---|---|
student |
/app |
Primary |
ops |
/admin/ops |
Primary |
ops_manager |
/admin/ops |
Approvals, refunds, overrides |
program_manager |
/admin/ops |
Batch ownership filters |
customer_support |
/admin/ops |
Tickets |
trainer |
Limited ops views | Roster |
academic_counselor |
/admin/ac, /sales |
Future-primary |
sales |
/sales |
Future-primary |
leadership |
All | Allowlist admin |
| Action | student | ops | ops_manager | leadership |
|---|---|---|---|---|
enrollment.read_own |
✅ | — | — | — |
batch.read |
— | ✅ | ✅ | ✅ |
batch.create |
— | ✅ | ✅ | ✅ |
batch_transfer.request |
✅ | ✅ | ✅ | ✅ |
batch_transfer.approve |
— | ✅ | ✅ | ✅ |
ticket.create |
✅ | — | — | — |
ticket.manage |
— | ✅ | ✅ | ✅ |
allowed_staff_email.create |
— | — | — | ✅ |
Full matrix: src/lib/auth/can.ts + planning docs (RBAC_MATRIX.md in planning bundle).
Note: Most mutations use Server Actions, not public REST. Below are Route Handlers and representative patterns.
GET /api/health{ "status": "ok" }POST /api/app/zoom/join
Cookie: sb-access-token=...
Content-Type: application/json
{ "sessionId": "uuid" }{ "joinUrl": "https://zoom.us/j/..." }POST /api/cron/process-notifications
Authorization: Bearer <CRON_SECRET>POST /api/cron/enrollment-lifecycle
Authorization: Bearer <CRON_SECRET>{ "expired": 2, "pausesActivated": 0, "transferTimeouts": 1 }// app/app/plan/actions.ts
'use server';
export async function requestPauseAction(input) {
const ctx = await getAuthContextOrThrow();
const pause = await requestEnrollmentPause({ ctx, data: input });
revalidatePath('/app/plan');
return { success: true, pauseId: pause.id };
}src/
├── app/
│ ├── (public)/ # Marketing landing
│ ├── auth/ # OTP, Google callback, denied
│ ├── app/ # Student portal
│ ├── admin/
│ │ ├── (console)/ops/ # Operations console
│ │ └── ac/ # Academic counselor
│ ├── sales/ # AC submission (future-primary)
│ └── api/
│ ├── cron/ # Scheduled workers
│ ├── health/
│ └── app/zoom/join/
├── components/
│ ├── ui/ # shadcn primitives
│ ├── student/ # Student feature UI
│ ├── ops/ # Ops feature UI
│ └── layout/ # Shells, nav, tables
├── lib/
│ ├── auth/ # can(), context, allowlist
│ ├── db/ # Drizzle schema + client
│ ├── services/ # Domain logic (THE core)
│ ├── integrations/ # Zoom, Resend adapters
│ ├── events/ # Domain event bus
│ └── utils/ # Pure formatters
├── scripts/ # Ops CLI + smoke tests
└── tests/ # Vitest unit + integration
drizzle/ # Versioned SQL migrations
Philosophy: services/{domain}/{verb}-{entity}.ts — discoverable, testable, transaction-friendly.
| Tool | Version |
|---|---|
| Node.js | 20+ |
| pnpm | 9+ |
| Supabase project | ap-south-1 recommended |
| Docker | Optional (for local Postgres) |
git clone <repository-url>
cd hub
pnpm install
cp .env.example .env.local
# Fill: NEXT_PUBLIC_SUPABASE_*, SUPABASE_SERVICE_ROLE_KEY, DATABASE_URL, CRON_SECRETpnpm db:apply:all # migrations 0001–0013
pnpm db:seed # plans, courses, email templatespnpm dev
# http://localhost:3000pnpm exec tsc --noEmit
pnpm lint
pnpm test
pnpm staging:gates -- student@example.com # needs DB + test student| Issue | Fix |
|---|---|
| OTP email not arriving | Configure Supabase custom SMTP (Resend); check redirect URLs |
user_not_found in scripts |
Set DEV_FOUNDER_EMAIL / OPS_VERIFY_EMAIL |
| Migration drift | pnpm db:sync-migration-ledger then pnpm db:apply:all |
| Zoom join disabled | Set zoom_meeting_id on session; open join window script |
| Variable | Required | Description |
|---|---|---|
NEXT_PUBLIC_SITE_URL |
✅ | Canonical URL for OTP redirects |
NEXT_PUBLIC_SUPABASE_URL |
✅ | Supabase project URL |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
✅ | Anon key (client-safe) |
SUPABASE_SERVICE_ROLE_KEY |
✅ | Server-only admin API |
DATABASE_URL |
✅ | Postgres pooler URL (:6543) |
CRON_SECRET |
✅ | Bearer token for cron routes |
RESEND_API_KEY |
App email queue | |
RESEND_FROM_EMAIL |
Verified sender | |
STAFF_OPS_ALLOWLIST |
✅ | Comma-separated Gmail |
STAFF_SALES_ALLOWLIST |
Sales portal access | |
ZOOM_ACCOUNT_ID |
✅ | S2S OAuth |
ZOOM_CLIENT_ID |
✅ | S2S OAuth |
ZOOM_CLIENT_SECRET |
✅ | S2S OAuth |
THINKIFIC_BASE_URL |
Deep links | |
HUB_PILOT_LAUNCH |
— | true for existing-student pilot UI |
STUDENT_SUPPORT_EMAIL |
— | Support card |
DEV_FOUNDER_EMAIL |
dev | Local AC/ops bypass |
See .env.example for full list. Validate with pnpm verify:prod-env.
The repo ships .do/app.staging.yaml — Node build, port 3000, env secrets in DO console.
# Build
pnpm install && pnpm build
# Start
pnpm start| Environment | Host |
|---|---|
| Staging | hub-staging.microdegree.work |
| Production | hub.microdegree.work |
Architecture is compatible (Next.js 16). Configure same env vars; set Cron via Vercel Cron or external scheduler hitting /api/cron/*.
- Separate project per environment.
- Enable
pgcrypto. - Auth → SMTP (Resend) + redirect URLs for
/auth/confirm,/auth/callback. - Backup before
pnpm db:apply:all.
| Schedule | Endpoint |
|---|---|
| Every 10 min | /api/cron/process-notifications |
| Hourly | /api/cron/ticket-sla |
| Daily 02:00 IST | /api/cron/ticket-auto-close |
| Daily 03:00 IST | /api/cron/enrollment-lifecycle |
curl -fsS -X POST \
-H "Authorization: Bearer $CRON_SECRET" \
"https://hub.microdegree.work/api/cron/process-notifications"Verify: pnpm staging:verify-host -- https://hub.microdegree.work
# Illustrative pipeline
- pnpm install --frozen-lockfile
- pnpm exec tsc --noEmit
- pnpm lint
- pnpm test
- pnpm build
# deploy to DO / Vercel- App: Redeploy previous container image / Vercel deployment.
- DB: Forward-only migrations — restore from Supabase backup if needed (last resort).
| Technique | Application |
|---|---|
| React Server Components | Zero client JS for read-heavy dashboards |
Selective use client |
Forms, join button, interactive tables only |
| Connection pooling | Supabase pooler; short-lived connections |
| Transactional writes | Consistent mutations without distributed locks |
| Path revalidation | Surgical cache bust after mutations |
| IST formatters | Pure functions; no runtime timezone guessing |
| Pagination | Ops queues (tickets, students) — cursor/offset patterns |
Not in V1: Redis, edge caching of authenticated pages, React Query — intentional simplicity.
V1 removed counselor-first onboarding from the critical path because:
- Running cohorts do not need sales workflow to attend Monday class.
- Every extra portal is a failure mode and training burden.
- Manual CSV preload guarantees data correctness before automation.
Hub optimizes for:
- Time-to-join (student).
- Time-to-assign (ops).
- Time-to-answer (support tickets).
When ops asks “who moved this student?” — audit_logs answers with actor, before/after, timestamp.
| Phase | Capability |
|---|---|
| V1 | Monolith, Postgres, cron HTTP |
| V1.5 | Razorpay webhooks, Tally AC, WhatsApp notifications |
| V2 | Event outbox, queue workers, read replicas |
| V3 | Multi-tenant isolation, regional deployments, mobile apps |
flowchart TB
subgraph Future["Future event-driven"]
API[Hub API]
OUTBOX[(outbox_events)]
WORKER[Queue workers]
WH[Webhooks Zoom/Razorpay/WATI]
end
API --> OUTBOX --> WORKER
WH --> API
- OTP login, dashboard, Zoom join, roadmap, ops CSV import, batch/session ops, tickets, workshops, pause/transfer.
- Native sales AC as primary acquisition path.
- Razorpay payment confirmation webhooks.
- Resend domain production hardening.
- Zoom attendance webhook ingestion.
- CRM sync (LeadSquared).
- WhatsApp BSP (WATI).
- Advanced analytics / ops dashboards.
- Student mobile PWA polish (Serwist groundwork exists).
- AI cohort recommendations.
- Predictive churn alerts.
- LMS deeper integration.
- Multi-program tenant model.
| Layer | Tool | Scope |
|---|---|---|
| Unit | Vitest | Services, utils, RBAC helpers |
| Integration | Vitest + real DB | AC verify → student, tickets, auth provision |
| Smoke | pnpm staging:gates |
Env + E2E service paths |
| Host | pnpm staging:verify-host |
Health + cron auth |
pnpm test
pnpm test:integration:db
pnpm test:integration:auth
pnpm smoke:post-migrate -- student@example.comFuture: Playwright for browser E2E, k6 load tests on join endpoint.
| Signal | V1 implementation |
|---|---|
| Health | GET /api/health — uptime checks |
| Audit | audit_logs — forensic |
| Cron success | HTTP status + JSON counts from cron routes |
| Logs | Structured server logs (avoid PII) |
Future stack candidates: Sentry, Better Stack, Axiom, OpenTelemetry.
main → production-ready
feature/* → scoped changes
tsc --noEmitclean.pnpm lintclean.- Service mutations include audit emission.
- No business logic in route files.
- One implementation pack per PR when possible.
feat(student): self-serve batch transfer on /app/batches
fix(zoom): join window timezone edge case
chore(db): apply 0013 enrollment changes migration
- Monolith + strict services beat premature microservices for a small ops-heavy team.
- State machines in code with Postgres enums — not free-form status strings — saved support hours.
- Pilot scope discipline prevented launch paralysis; CSV import unlocked real users faster than perfect sales flow.
- Supabase Auth + custom SMTP separation confused teams until documented (Auth email ≠ Resend app email).
- Audit logs paid off the first time ops asked “who changed this enrollment?”
Replace placeholders with real captures before investor demo.
| Screen | Placeholder |
|---|---|
| Student dashboard | docs/assets/student-dashboard.png |
| Roadmap | docs/assets/roadmap.png |
| Ops student detail | docs/assets/ops-student-detail.png |
| Batch sessions | docs/assets/batch-sessions.png |
Proprietary. © MicroDegree. All rights reserved.
Unauthorized copying, distribution, or use of this software is prohibited.
| Channel | Link |
|---|---|
| Product | microdegree.in |
| Student hub | hub.microdegree.work |
| Support | support@microdegree.work |
Built with discipline for operators and students who deserve a calmer Monday morning.
⭐ If this README helped you onboard — star the repo.