Immigr8's core product flows and the technical worker process built to solve each one. Ground-truth as of the live system (owned async evaluation pipeline went end-to-end live 2026-06-13).
A staff evaluator opens a submission and clicks “Run Immigr8 Evaluation” → POST /submissions/:id/evaluate. Gated by a global kill-switch (EVAL_PIPELINE_ENABLED) and a per-country allow-list (EVAL_ENABLED_COUNTRIES). It inserts one row into the evaluation_jobs Postgres queue with status queued. A partial-unique index guarantees only one active job per submission — a re-run while a draft is in flight returns the existing job (202 deduped).
The immigr8-worker — a separate Render process running the same apps/api tree (node dist/worker.js) — polls every 5 seconds. It claims the oldest queued job with SELECT … FOR UPDATE SKIP LOCKED, flips it to running, stamps claimed_at and bumps attempts. Jobs stuck running > 10 min are reclaimed to queued; after 3 attempts a job goes failed. A health check emits structured warnings on failure spikes or queue backlog.
For each job, the worker POSTs an r2Key to a Cloudflare Container running PaddleOCR (PP-StructureV3, Apache-2.0) at EXTRACT_ENDPOINT_URL. The container pulls the transcript bytes straight from R2 (immigr8-transcripts, free same-network egress) and returns ExtractedCourse[] — { foreignCourse, grade, yearTaken, gradeLevelHint, ocrConfidence }. A 240 s timeout plus a provisioning-503 retry (up to 9× / 20 s apart) absorbs the scale-to-zero container's ~150 s cold start. Multiple documents are merged + de-duplicated; supporting docs are skipped.
Commodity, swappable: reading is not the IP. The earlier Modal plan was superseded by the owned Cloudflare Container (decision 2026-06-08).
evaluateTranscriptDeterministic() maps the structured courses to US equivalents with no LLM in the loop. Per course it consults a tiered source of truth:
@immigr8/shared) — Sandy's curated per-subject mappings + grading scales for the curated countries. → sourceOfTruth=manual, confidence 90.sourceOfTruth=ai_generic, confidence 20, “specialist mapping required.”Then: grade conversion via the Manual's scale bands; credit rules (a course at the student's current grade = 0 credit / in_progress_us, else 1.0 split 0.5 fall + 0.5 spring); STAAR eligibility; and grade placement = highest completed grade + 1.
A 15-rule validator (IMMIGR8-354) scores every mapped row. BLOCK rows are dropped (grade out of 1–12 range, future year, ascending-grade/year inversion, duplicate course, malformed TWEDS code…); WARN rows are kept with flags attached (low_confidence, low_ocr_confidence < 0.6, missing source quote…). The validator is a second safety net regardless of which engine produced the draft.
persistAiDraft() discards any prior isAiDraft=true rows (a specialist's hand-entered rows are never touched), sanitizes every field to satisfy the DB CHECK constraints, then writes course_mappings + evaluations with isAiDraft=true plus the AI metadata: aiConfidence, validationFlags, aiProvenance, sourceOfTruth.
POST /submissions/:id/approve-ai-draft pre-flight-blocks approval if any row still carries a flag or confidence < 0.70 (unless a platform_admin forces it). On approval it flips isAiDraft true → false on every row + the evaluation. After the flip the record is operationally identical to a hand-typed one; the AI-metadata columns are retained for audit and Path-3 training capture.
/ai-analyze) still exists and writes through the same persistAiDraft(), but the deterministic engine is the live owned path. Losing an API key can never break the core evaluator. (Decision 2026-06-05.)submissionsA district uploads a transcript for Immigr8 staff to evaluate. Status workflow: submitted → assigned → in_review → completed. This is the active path that the Flow 1 pipeline runs on today — evaluation_jobs.submission_id points here.
transcriptReviewsA district user reviews a transcript independently in the portal. Simpler status: draft → in_progress → completed. The table exists and is wired, but is dormant today; the owned pipeline writes through the submissions path. (Per the reconciliation ADR, the reviews path is untouched.)
immigr8-transcripts, 50 MB max, 1-hour presigned URLs) and both eventually produce a course_mappings + evaluations pair.One submission → N course_mappings + one evaluation. There is deliberately no separate “AI evaluations” table — that would fork every downstream feature into two code paths.
Manual entry calls createForSubmission / upsertForSubmission with isAiDraft:false. The bot calls the identical repos with isAiDraft:true + the AI-metadata columns. The only difference between the two records is that flag.
approveAiDraftForSubmission() sets isAiDraft=false on every draft row + the evaluation. Safety invariants: discard only deletes isAiDraft=true rows (specialist work is safe), the enqueue endpoint dedupes an active job (no race), and AI metadata is retained post-approval for the audit trail + the Path-3 training set.
findOrCreateStudent() runs as a submission is created. A strong match (first-word-of-first-name + last name + exact date of birth) reuses the existing record; a weak match (fuzzy name + DOB within tolerance) is flagged for human review; no match creates a new students row. The chosen path is captured in the activity log with a confidence + source.
Matching never crosses districts. Cross-district student linkage requires a platform_admin action + an audit-log entry — the same RLS posture as submissions.
A pure scoring function (scoreStudentPair) powers a duplicates view that suggests merges; a specialist confirms each one. The dry-run backfill of historical duplicates is human-gated before --apply.
Stripe fires checkout.session.completed → POST /billing/webhook (signature-verified). The Stripe session id is stored for idempotency so a re-delivered webhook can't double-provision.
The handler creates a districts row (name, size band, plan tier, subscriptionStatus=active, state, contact), a campuses row for a campus-level signup, and a users row as district_admin or campus_admin. Both creations are written to the activity log.
scheduleWelcomeSequence() queues the branded onboarding emails (SendGrid). No human touches the account between payment and a usable login.
training_pair. When the set crosses a decision gate (IMMIGR8-275), those pairs fine-tune an owned Immigr8 model for Phase A reading + the uncovered tail of Phase B — with the deterministic engine + the 15-rule validator staying on as the guardrail that keeps the model honest and auditable. Fully owned, end to end.
evaluation_jobs queue contract