Backlog— 31 tickets

~3 min
IMMIGR8-30
Complete email migration
High (P1)jakeCLAUDE
Backlog
IMMIGR8-31
Shared inbox for team@immigr8.net
Medium (P2)jakeCLAUDE
Backlog
IMMIGR8-32
Verify aliasing
Medium (P2)jakeCLAUDE
Backlog
IMMIGR8-33
Add all Immigr8 Gmail users to my Immigr8 shared Drive
Medium (P2)jakeCLAUDE
Backlog
IMMIGR8-177
April 3: MX migration to Google Workspace + DKIM setup
High (P1)jakeCLAUDE
Backlog
IMMIGR8-230
Unsuspend sandy@ and presley@ in Google Admin
Medium (P2)jakeCLAUDE
Backlog
IMMIGR8-231
Execute email migration: GoDaddy PPE to Google Workspace
High (P1)jakeCLAUDE
Backlog
IMMIGR8-234
[Workspace Anchor 18/22] Follow-up: Verify Immigr8 Gmail DWD works after Friday 4/17 migration
Medium (P2)jakeCLAUDE
Backlog
IMMIGR8-277
Cut over immigr8.net DNS from Kinsta WordPress to Render landing site
Urgent (P0)jakeMANUAL
✓ Acceptance criteria
  • Add immigr8.net (apex) + www.immigr8.net as custom domains on immigr8-landing Render service via Render dashboard
  • Update Cloudflare DNS for [immigr8.net](<http://immigr8.net>): change root A/AAAA records (currently → Kinsta IPs 104.21.35.130 / 172.67.222.144 proxied via CF) to point to Render's CNAME (typically <service>.onrender.com or Render's flex IP)
  • Render auto-provisions Let's Encrypt SSL for the apex + www in 5-15 min
  • Verify curl -sI https://immigr8.net/favicon.svg returns 200 with image/svg+xml content-type
  • Verify curl https://immigr8.net/privacy, /terms, /subprocessors all return 200
  • Verify the page title at [immigr8.net](<http://immigr8.net>) root is the new "Immigr8 - Support for Today's Global Students"
  • Decide on Kinsta WordPress fate: cancel hosting (\~$30-50/mo savings) OR keep as backup for 30 days OR archive content elsewhere
Backlog
IMMIGR8-287
Sandy QA Sprint v1.0 — pre-alpha comprehensive testing (parent epic)
Urgent (P0)sandyCLAUDE
Backlog
IMMIGR8-288
Sandy QA: Landing page + marketing surface
High (P1)sandyCLAUDE
Backlog
IMMIGR8-289
Sandy QA: Auth + onboarding journeys (5 flows)
Urgent (P0)sandyCLAUDE
Backlog
IMMIGR8-290
Sandy QA: Submission lifecycle (create → upload → status)
Urgent (P0)sandyCLAUDE
Backlog
IMMIGR8-291
Sandy QA: AI Mexico evaluator end-to-end (Diego + Eder, all 4 phases)
Urgent (P0)sandyCLAUDE
Backlog
IMMIGR8-292
Sandy QA: Course mappings + STAAR/EOC editing
High (P1)sandyCLAUDE
Backlog
IMMIGR8-293
Sandy QA: PDF + certificate quality
High (P1)sandyCLAUDE
Backlog
IMMIGR8-294
Sandy QA: Country + curriculum data accuracy (35+ countries)
High (P1)sandyCLAUDE
Backlog
IMMIGR8-295
Sandy QA: Settings + RBAC enforcement (7 roles)
Urgent (P0)sandyCLAUDE
Backlog
IMMIGR8-296
Evaluate Seamless AI for lead gen
jakeCLAUDE
Backlog
IMMIGR8-309
Complete Ticket 242 Waves 3–9 via Kinsta SSH/WP CLI
jakeCLAUDE
Backlog
IMMIGR8-312
Massage 1Password creds to align live/test; rename courses to match new lifecycle
jakeCLAUDE
Backlog
IMMIGR8-314
Migrate Immigr8 storage to Render Postgres; fix PDF attachments
jakeCLAUDE
Backlog
IMMIGR8-316
Complete G Suite migration May 8 2 pm; confirm Gmail access
jakeCLAUDE
Backlog
IMMIGR8-318
Provide Sandra dictation app setup; enable voice-to-text testing
sandyCLAUDE
Backlog
IMMIGR8-319
Attend AI 101 training Jun 16 & 18 8–12; take breaks
sandyCLAUDE
Backlog
IMMIGR8-320
Purchase AI 101 course via Jake's link w/ 50% code
sandyCLAUDE
Backlog
IMMIGR8-321
Post-cutover cleanup: cancel Kinsta WordPress hosting subscription
Medium (P2)jakeMANUAL
✓ Acceptance criteria
  • IMMIGR8-277 cutover redone and stable for ≥7 days (verify via uptime + no rollback events)
  • Export WP content as a backup tarball (WP CLI or Kinsta dashboard export) → save to Drive vault under Immigr8/legacy-wordpress-backup-2026-MM-DD.tar.gz
  • Note any WP-CLI / SSH credentials that should be saved before access goes away (per IMMIGR8-309 + future data needs)
  • Log into Kinsta dashboard → cancel immigr8 site subscription
  • Confirm next month's invoice no longer charges for the cancelled site
  • Update entity matrix (reference-entity-matrix.md) to remove Kinsta from Immigr8's vendor list
  • Update ~/.claude/projects/-Users-imac/memory/vision-immigr8.md to remove WordPress as a "still live" system
Backlog
IMMIGR8-322
Post-cutover cleanup: declare immigr8.net custom domains in landing/render.yaml
Medium (P2)jakeCLAUDE
✓ Acceptance criteria
  • IMMIGR8-277 cutover is live + stable
  • Add customDomains: [name: "immigr8.net", name: "www.immigr8.net"] (or equivalent) to the immigr8-landing service block in landing/render.yaml
  • Add the same to the root render.yaml's landing service block for consistency
  • Commit + push: chore(render): declare immigr8.net + www custom domains on landing service
  • No live config change — yaml-only update (custom domains are already set via API per IMMIGR8-277 work)
Backlog
IMMIGR8-323
Post-cutover cleanup: audit + update docs/memory referencing WP/Kinsta as live
Medium (P2)jakeCLAUDE
✓ Acceptance criteria
  • IMMIGR8-277 cutover is live + stable
  • Audit + update:
  • /Users/imac/Developer/immigr8/CLAUDE.md — § Legacy Stack (WordPress — Still Live) → mark as "Decommissioned MM/YYYY"
  • /Users/imac/Developer/immigr8/README.md (if it references the WP stack)
  • ~/.claude/projects/-Users-imac/memory/vision-immigr8.md — remove "WordPress migration" references; reframe as completed
  • docs/product-analysis.md + docs/rebuild-plan.md — mark WP phases as complete
  • Sales-collateral audit: any PDFs, slide decks, sales emails referencing [immigr8.net](<http://immigr8.net>) as WP-hosted → flag with Sandy for update
  • Update reference-entity-matrix.md to remove WP/Kinsta from Immigr8 vendor list (paired with IMMIGR8-NNN cancellation ticket)
Backlog
IMMIGR8-324
Complete app migration
High (P1)jakeMANUAL
Backlog
IMMIGR8-326
Auth GCP w/ Immigr8.net; complete remaining 12 pre-migration tasks
jakeMANUAL
Backlog
Discussion:Quick scan - any of these worth promoting to Todo or canceling?

To-do— 9 tickets

~3 min
IMMIGR8-331
Build proprietary Immigr8 AI evaluator (Path 3 implementation epic)
Urgent (P0)UnassignedCLAUDE
To-do
IMMIGR8-334
Student record deduplication - intake matcher + admin merge UI + backfill
High (P1)UnassignedCLAUDE
P2 · Student dedup engineering - 17.7% dup rate (1051/5947); next major rock
✓ Acceptance criteria
  • Intake matcher - POST /api/submissions (and any other student-creating path) calls a new findOrCreateStudent({firstName, lastName, dateOfBirth?, countryOfOriginId?}) helper that searches for an existing match using normalized name + (optional) DOB + (optional) country before creating new. Defined match keys + tie-breaker docs land in apps/api/src/lib/student-matching.ts with unit tests.
  • Admin duplicates view - /settings/students/duplicates (platform_admin + platform_team) lists candidate dup groups (same shape as UserDuplicates). Each group shows the records + their submissions + a "Merge into" action. Soft-merge sets students.merged_into_id and re-points submissions.student_id to the canonical record; deleted students get deleted_at set, not hard-deleted.
  • Backfill migration - apply-student-dedup-merge.ts runs in dry-run mode first, produces a report at assets/raw/<date>-student-dedup-candidates.csv for Sandy/Jake to review BEFORE any writes. Live mode requires --apply flag.
  • Eder Gael verified - after backfill, b05e0404 and e93f875b collapse to one canonical record with both submissions attached. View student record link from either submission reaches the same /students/<id> page.
  • No new duplicates created - submissions for "Eder Hernandez" intake after the matcher lands link to the same student row.
To-do
IMMIGR8-340
DR Congo manual content seed (Sandy to supply JSON)
Medium (P2)UnassignedMANUAL
P3 · DR Congo manual seed script ready; awaiting Sandy curriculum JSON
✓ Acceptance criteria
  • Sandy supplies the DR Congo curriculum content (education system overview, grade levels, grading scale, subject mappings, credit system, evaluator notes) in the standard manual.content shape - see sample JSON in the seed script
  • Jake runs: pnpm tsx apps/api/src/db/scripts/seed-dr-congo-manual.ts /path/to/content.json
  • /countries/<dr-congo-id> page renders all 8 sections with curated content (no more 'Data not yet supplied' placeholders for Immigr8-curated sections)
To-do
IMMIGR8-341
Fix R2/Cloudflare blob storage error preventing evaluation views (app + staging)
Urgent (P0)UnassignedCLAUDE
P2 · Standup unification refactor (this work) - index/live/analytics share one data source
✓ Acceptance criteria
  • Diagnose which API key / binding is failing (R2 access key vs Workers binding vs different storage)
  • Confirm against 1Password ("Immigr8" vault) which key is current
  • Update Render env vars on immigr8-api-staging and production API service
  • Verify by opening an existing evaluation (Eder Gael Hernandez or Diego Meza) on both staging and prod
  • Add a startup-time R2 connectivity check that logs a clear FATAL/WARN if blob storage is unreachable
To-do
IMMIGR8-342
Fix "Run AI Analysis" error on submission detail (Diego Meza repro)
Urgent (P0)UnassignedCLAUDE
✓ Acceptance criteria
  • Reproduce the error, capture exact response status + body
  • Fix root cause
  • Replace generic toast with a clear in-UI error message Sandy can screenshot back to us (status code + reason)
  • Verify on Diego Meza and at least one other submission
To-do
IMMIGR8-343
Student-record dedup: fuzzy-match prompt on intake (prevent duplicate student profiles)
High (P1)UnassignedCLAUDE
✓ Acceptance criteria
  • On intake, run a fuzzy match against existing students in the district (name + DOB + country of origin)
  • If 1+ likely match: show modal "There seems to be a previous Eder Gael Hernandez profile — do you want to map this evaluation to that student?"
  • If multiple candidates: list them with disambiguators (DOB, school, last eval date)
  • Option: "No, this is a new student" creates a new record
  • Audit log entry for which choice was made
  • Backfill/merge tool in HQ for existing duplicates — Eder Gael Hernandez specifically: link the two records, choose canonical, merge eval history
To-do
IMMIGR8-344
Verify-flow UX: one-click 'Mark Verified' on dev.immigr8.net briefing cards
Medium (P2)UnassignedCLAUDE
✓ Acceptance criteria
  • Each In Review ticket card on [dev.immigr8.net](<http://dev.immigr8.net>) shows a clear "Mark Verified" button
  • Button POSTs to api-staging which moves the Linear ticket from In Review → Done via Linear API
  • Button enabled when ticket assignee == Sandy (or platform_admin viewer)
  • Confirmation modal: "Mark IMMIGR8-XXX as verified and move to Done?"
  • On success: card visibly moves to Done section (or fades out with toast)
  • One-line training note in briefing header: "After testing a feature, click Mark Verified to move it to Done."
To-do
IMMIGR8-346
Off-Render DB backup automation (nightly pg_dump → R2)
High (P1)UnassignedCLAUDE
✓ Acceptance criteria
  • Render cron job (or scheduled GitHub Action) runs nightly at 0700 CT
  • pg_dump --format=custom both immigr8-db-staging and immigr8-hq-db
  • Compress (gzip) and upload to Cloudflare R2 bucket immigr8-db-backups (separate from immigr8-transcripts)
  • Encrypted at rest (R2 default), key/object naming: {db-name}/{YYYY-MM-DD}.dump.gz
  • 30-day retention policy on the bucket (lifecycle rule)
  • Document restore procedure in management/plans/ (one-liner: psql $DATABASE_URL -f restore.sql from a backup)
  • Alert on failure (SendGrid email to [jake@1116.info](<mailto:jake@1116.info>))
To-do
IMMIGR8-348
Course-code alignment validator + backfill audit (catch AI mis-mappings)
High (P1)UnassignedCLAUDE
✓ Acceptance criteria
  • Validation library - apps/api/src/lib/course-code-validator.ts exposes validateCourseMapping({usEquivalent, courseCode}) returning {ok: boolean, expectedTitle?: string, mismatch?: 'code-not-in-tweds'|'title-mismatches-code'|'title-has-embedded-different-code'}. Uses TWEDS_CATALOG from @immigr8/shared for canonical lookups.
  • Server-side enforcement - POST /api/submissions/:id/mappings and the AI-draft writer both run the validator. On mismatch: log a warning + persist the record but flag it (new column course_mappings.code_alignment_status enum: verified, warning, unchecked).
  • HQ surface - the evaluator UI (SubmissionDetail / CourseMappings table) shows a warning chip on flagged rows with the suggested correct title or code, so Sandy can one-click fix during review.
  • Backfill audit script - apps/api/src/db/scripts/audit-course-code-alignment.ts scans every existing course_mappings row, emits a CSV at assets/raw/<date>-course-code-misalignments.csv with: submission_id, student_name, grade_level, foreign_course, current us_equivalent, current code, expected title, recommended fix. Dry-run by default; --apply mode applies the recommendations only when high-confidence (code IS in TWEDS, us_equivalent close-match to title via Levenshtein < 3).
  • Quantify drift - first dry-run produces a report; Sandy + Jake review it before any auto-apply
  • Regression test - test fixture covers the 3 Dasom rows fixed in IMMIGR8-342 so the validator catches them retrospectively
To-do
Discussion:Anything here we'd like to bump into In Progress this week? Or punt to Backlog?

In progress— 6 tickets

~12 min
IMMIGR8-269
Compliance: enable Anthropic zero-data-retention on Immigr8 workspace
Urgent (P0)jakeMANUAL
✓ Acceptance criteria
  • Zero-retention is enabled (per console or per Anthropic confirmation)
  • Confirmation artifact saved to vault
  • Updated landing/subprocessors.html if needed (current language is forward-compatible)
  • Privacy Policy (IMMIGR8-264) reflects zero-retention status
  • DPA template (IMMIGR8-265) references the Anthropic posture
  • Sales/support team can answer "does Anthropic see our students' transcripts long-term?" with: "No — we run on Anthropic's zero-retention mode, transcripts are processed and immediately discarded by Anthropic"
In progress
IMMIGR8-278
Alpha coverage: Mexico + India + Venezuela + Vietnam + El Salvador AI evaluator readiness
Urgent (P0)sandyMANUALCLAUDE
P1 · Revised 2026-05-18 - manual is canonical source of truth; spike before per-country migration
✓ Acceptance criteria
  • One source of truth for country mapping rules - the in-product manual, not the TypeScript file
  • Sandy can edit rules for one country (India as the proof) entirely within the product, without engineering involvement
  • AI evaluator picks up the updated rules on next evaluation run (cache invalidation on manual edit)
  • The 4 existing country-mapping stubs (India, Venezuela, Vietnam, El Salvador) are wired to the manual data instead of pending Sandy's TS file edits
  • Documentation in docs/decisions/2026-05-18_country-rules-source-of-truth.md capturing the architecture decision
🚧 Owner: Sandy · All 4 sub-issues (IMMIGR8-279/280/281/282) need Sandy's curriculum-domain content.
In progress
IMMIGR8-279
AI evaluator: India mapping rules + tests + Sandy-validated transcript run
Urgent (P0)sandyMANUALCLAUDE
P1 · Paused pending IMMIGR8-278 ADR
✓ Acceptance criteria
  • <UPPER_SLUG>_RULES exported from country-mapping-rules.ts with the same shape as MEXICO_RULES
  • Registered in the country-code → rules lookup so analyzeTranscript picks it up automatically
  • ≥20 unit tests in country-mapping-rules.test.ts covering native-language LOTE pathway, English → ESOL, Math → Algebra family, grade-scale boundaries, recommended-course chain
  • Run run-ai-analysis-on-submission.ts on at least one real India student transcript and have Sandy validate the resulting draft
  • activate-immigr8-257-schema.ts NOT needed (no DB changes — rules are in shared module)
🚧 Owner: Sandy · India curriculum-domain spec. Approximate effort: 60-90 minutes of focused writing.
In progress
IMMIGR8-280
AI evaluator: Venezuela mapping rules + tests + Sandy-validated transcript run
Urgent (P0)sandyMANUALCLAUDE
P1 · Paused pending IMMIGR8-278 ADR
✓ Acceptance criteria
  • <UPPER_SLUG>_RULES exported from country-mapping-rules.ts with the same shape as MEXICO_RULES
  • Registered in the country-code → rules lookup so analyzeTranscript picks it up automatically
  • ≥20 unit tests in country-mapping-rules.test.ts covering native-language LOTE pathway, English → ESOL, Math → Algebra family, grade-scale boundaries, recommended-course chain
  • Run run-ai-analysis-on-submission.ts on at least one real Venezuela student transcript and have Sandy validate the resulting draft
  • activate-immigr8-257-schema.ts NOT needed (no DB changes — rules are in shared module)
🚧 Owner: Sandy · Venezuela curriculum-domain spec. Approximate effort: 45-60 minutes of focused writing.
In progress
IMMIGR8-281
AI evaluator: Vietnam mapping rules + tests + Sandy-validated transcript run
Urgent (P0)sandyMANUALCLAUDE
P1 · Paused pending IMMIGR8-278 ADR
✓ Acceptance criteria
  • <UPPER_SLUG>_RULES exported from country-mapping-rules.ts with the same shape as MEXICO_RULES
  • Registered in the country-code → rules lookup so analyzeTranscript picks it up automatically
  • ≥20 unit tests in country-mapping-rules.test.ts covering native-language LOTE pathway, English → ESOL, Math → Algebra family, grade-scale boundaries, recommended-course chain
  • Run run-ai-analysis-on-submission.ts on at least one real Vietnam student transcript and have Sandy validate the resulting draft
  • activate-immigr8-257-schema.ts NOT needed (no DB changes — rules are in shared module)
🚧 Owner: Sandy · Vietnam curriculum-domain spec. Approximate effort: 60-90 minutes of focused writing.
In progress
IMMIGR8-282
AI evaluator: El Salvador mapping rules + tests + Sandy-validated transcript run
Urgent (P0)sandyMANUALCLAUDE
P1 · Paused pending IMMIGR8-278 ADR
✓ Acceptance criteria
  • <UPPER_SLUG>_RULES exported from country-mapping-rules.ts with the same shape as MEXICO_RULES
  • Registered in the country-code → rules lookup so analyzeTranscript picks it up automatically
  • ≥20 unit tests in country-mapping-rules.test.ts covering native-language LOTE pathway, English → ESOL, Math → Algebra family, grade-scale boundaries, recommended-course chain
  • Run run-ai-analysis-on-submission.ts on at least one real El Salvador student transcript and have Sandy validate the resulting draft
  • activate-immigr8-257-schema.ts NOT needed (no DB changes — rules are in shared module)
🚧 Owner: Sandy · El Salvador curriculum-domain spec. Approximate effort: 30-45 minutes — **fastest of the four** since the structure parallels Mexico (Spanish-native, 0-10 grade scale, similar subject taxonomy).
In progress
Discussion:Owner check + blocker triage for each row.

Review— 41 tickets

~10 min
IMMIGR8-236
Automated STAAR Assessment Status with Default “Assessment Required” and Eligibility-Based Qualification
Medium (P2)sandyCLAUDE
P3 · Dasom Kim EOC fix shipped Apr 29 - re-verify post EOC-matcher upgrade (IMMIGR8-337)
✓ Acceptance criteria
  • Five STAAR assessments are tracked per evaluation: English I, English II, Algebra I, Biology, U.S. History
  • On evaluation creation, all five STAAR assessments default to status assessment_required
  • System auto-promotes a STAAR status to not_required_criteria_met when a corresponding course mapping exists with grade ≥ 70 AND credits ≥ 1 (full credit)
🚧 Owner: Sandy · Re-verify the Dasom Kim EOC exemption fix on the staging app. Confirm the English II EOC is no longer marked exempt without the English II course present + passing.
Review
IMMIGR8-238
Build Country-Level Academic Framework Database from NCAA International Guide
Low (P3)sandy
P4 · Country detail UX standardized (IMMIGR8-338) - inline NCAA + data-missing placeholders
🚧 Owner: Sandy · Confirm the NCAA framework is appearing where she expected it — Sandy's original ask was for it to "fill holes for countries we don't have any data for, outside our predesigned 35 frameworks."
Review
IMMIGR8-249
Standardize and Validate Texas PMCO22 (C022/TWEDS) Course Table – Leading Zero Enforcement & Course Completeness
Low (P3)sandy
P4 · 1175 codes validated, extended scope shipped - probably ready to close
🚧 Owner: Sandy (or Jake — Sandy may have already implicitly approved) · Final close — confirm no remaining concerns on the TWEDS catalog validation work.
Review
IMMIGR8-257
Create AI Bot to Read Foreign Transcript Records and Generate Draft Transcript Evaluations
Medium (P2)sandy
P3 · Alpha gate. Diego & Eder validation runs queued May 4 - has Sandy reviewed the AI drafts?
🚧 Owner: Sandy · Review the AI drafts produced for Diego Alonso Meza + Eder Hernandez (Mexico-origin test submissions queued 2026-05-04). Judge whether the AI's course mappings, TWEDS code recommendations, and academic findings are accur
Review
IMMIGR8-258
Security: assert JWT_SECRET is non-default in production at boot
Urgent (P0)UnassignedCLAUDE
✓ Acceptance criteria
  • At boot, when NODE_ENV === 'production', the process throws a fatal error if any of: JWT_SECRET is unset / equals 'dev-secret-change-me' / length < 32
  • The error message names the offending variable + remediation hint
  • A debug log line at startup prints SHA-256 prefix of the loaded secret (8 chars) so misconfig is visible across environments without leaking the secret
  • When production service is added, JWT_SECRET is provisioned separately (not shared with staging via single generateValue: true blueprint reuse)
  • Documented quarterly rotation procedure in a runbook (management/runbooks/jwt-secret-rotation.md)
Review
IMMIGR8-259
Security: upgrade drizzle-orm past SQL identifier injection CVE (≥0.45.2)
Urgent (P0)UnassignedCLAUDE
✓ Acceptance criteria
  • apps/api/package.jsondrizzle-orm upgraded to ≥ 0.45.2 (latest stable preferred)
  • apps/api/package.jsondrizzle-kit upgraded to compatible version
  • pnpm typecheck clean in apps/api + packages/shared
  • pnpm test 236/236 still passes in apps/api
  • pnpm audit no longer lists the drizzle-orm HIGH CVE
  • Manual smoke: load a submission detail in staging UI, confirm queries still return correct shape
  • Run apply-immigr8-257-schema.ts (or similar idempotent migration) to verify drizzle-kit still works with the new version
Review
IMMIGR8-260
Security: scope + audit-log GET /submissions/export and PATCH /submissions/bulk
Urgent (P0)UnassignedCLAUDE
✓ Acceptance criteria
  • GET /submissions/export accepts optional ?districtId=<uuid> query param
  • When districtId is provided, results are filtered to that district
  • When districtId is omitted: results are page-capped at 1000 rows + the response includes truncated: true + a hint to use districtId for full export
  • Every call to GET /submissions/export writes a logActivity row with action='exported', metadata={ rowCount, districtIdFilter, scope: 'all' | 'district' }
  • Same logActivity treatment for PATCH /submissions/bulk (with metadata.changedIds)
  • activity_action enum extended to include 'exported' if not already present
  • No regression on per-district workflows (singular endpoints still scope correctly)
Review
IMMIGR8-261
Security: gate GET /sso-configs behind authenticate + platform_admin
Urgent (P0)UnassignedCLAUDE
✓ Acceptance criteria
  • GET /sso-configs requires authenticate + requireRole('platform_admin') middleware
  • Anonymous request to /api/sso-configs returns 401 Unauthorized
  • Authenticated district_team/district_admin/campus_*/reporting/platform_team request returns 403 Forbidden
  • Authenticated platform_admin request returns the configs list as before
  • Activity log entry written on every successful read with action='read', metadata={ entityType: 'sso_configs' }
  • Code header comment matches what the middleware actually does
Review
IMMIGR8-262
Security: per-user rate limit on POST /chat/message (Anthropic cost protection)
Urgent (P0)UnassignedCLAUDE
✓ Acceptance criteria
  • POST /chat/message has a per-user rate limit using express-rate-limit with keyGenerator: (req) => req.user!.userId
  • Default limit: 30 messages / 5 minutes per authenticated user (configurable via env var CHAT_RATE_LIMIT_MAX + CHAT_RATE_LIMIT_WINDOW_MS)
  • When limit exceeded, returns 429 with { error, retryAfter } payload + Retry-After header
  • Activity log entry on rate-limit hit (action='rate_limited', metadata={ endpoint: 'chat/message' }) so abuse patterns are observable
  • Optional: per-district daily token cap stored in platform_settings.maxChatTokensPerDay — defer if scope creep, otherwise add a stub column
Review
IMMIGR8-263
Security: magic-byte file upload validation + remove /uploads static handler in prod
Urgent (P0)UnassignedCLAUDE
✓ Acceptance criteria
  • Add file-type (npm package) magic-byte verification after multer parses the upload buffer in validateFile (or a new validateFileMagic helper)
  • Reject upload if magic bytes don't match the declared MIME OR don't match an allowed type (PDF / JPEG / PNG / GIF)
  • validateFile returns descriptive error: "File appears to be <detected> but was uploaded as <claimed>"
  • Inconsistent file-size limit fixed: lib/storage.ts:9 says 128MB, multer config in routes/submissions.ts:27 caps at 50MB. Reconcile to 50MB everywhere (matches IMMIGR8-215 decision)
  • Remove app.use('/uploads', express.static(...)) registration in production OR conditionally register only when NODE_ENV !== 'production'
  • Add unit tests for the magic-byte validator: PDF magic ✓, JPEG magic ✓, PNG magic ✓, GIF magic ✓, plain text + Content-Type=PDF rejected, HTML + Content-Type=PDF rejected, executable + Content-Type=PDF rejected
Review
IMMIGR8-264
Compliance: rewrite Privacy Policy + ToS (real, not placeholder) — TX HB 18 + CCPA + sub-processors + AI
Urgent (P0)UnassignedMANUAL
✓ Acceptance criteria
  • Privacy Policy at landing/privacy.html (or new dedicated page) covering:
Review
IMMIGR8-265
Compliance: build DPA template + Texas SDPA template (TXSPA-aligned) for district contracts
Urgent (P0)UnassignedMANUAL
✓ Acceptance criteria
  • DPA template in legal/dpa-template.docx (or .md if Markdown-friendly) covering:
Review
IMMIGR8-266
Compliance: publish sub-processor list + execute vendor DPAs (Anthropic, SendGrid, Stripe, Cloudflare, Render)
Urgent (P0)UnassignedCLAUDE
✓ Acceptance criteria
  • New page: landing/subprocessors.html (or section in landing/privacy.html) listing each sub-processor with:
Review
IMMIGR8-267
Compliance: instrument logActivity in auth/billing/users/settings (SOC 2 CC7.2 + state law compliance)
Urgent (P0)UnassignedCLAUDE
✓ Acceptance criteria
  • Auth events in auth.ts:
  • Successful login (action='auth_login', metadata={ provider: 'password|google|microsoft|saml' })
  • Failed login (action='auth_login_failed', metadata={ email_hash: sha256(email), reason: 'unknown_user|wrong_password|locked|unverified' })
  • Logout (action='auth_logout', metadata={ tokenHashPrefix: first 8 chars })
  • Password change (action='password_changed')
  • Email verification (action='email_verified')
  • Billing events in billing.ts:
  • Checkout session created (action='checkout_initiated', metadata={ tier, sessionId })
  • +13 more in the Linear ticket
Review
IMMIGR8-268
Compliance: bind cyber liability + E&O insurance (M / M baseline) for district RFP eligibility
Urgent (P0)UnassignedMANUAL
✓ Acceptance criteria
  • Get quotes from 2-3 EdTech-specialist brokers
  • Decide on coverage limits (recommended baseline: $2M cyber + $1M E&O for first year, scale with ARR)
  • Bind policies + receive certificate of insurance (COI)
  • COI saved to 1Password vault Immigr8 as Insurance — COI 2026.pdf
  • COI uploadable to district procurement portals on demand
  • Renewal calendar reminder set (typically annual)
  • Cyber attorney on retainer for breach response (separate but related — surface in ticket if not yet engaged)
Review
IMMIGR8-270
Rotate immigr8-api-staging JWT_SECRET to 64-char hex value
High (P1)UnassignedMANUAL
✓ Acceptance criteria
  • Rotate JWT_SECRET on immigr8-api-staging to a freshly-generated 64-char hex value (openssl rand -hex 32)
  • Set the rotated value via Render dashboard (Settings → Environment → JWT_SECRET → edit). Use the GET-then-PUT-individual-key safe pattern per feedback-render-env-vars-safe-pattern.md — never bulk-PUT.
  • Save the previous value somewhere safe (1Password vault Immigr8 as Render env — immigr8-api-staging — pre-rotation 2026-05-02) for \~7 days in case rollback is needed
  • Confirm staging boots clean: [startup] JWT_SECRET loaded (sha256 prefix: <new-prefix>, env: production) — NO WARN line
  • All currently-issued staging JWTs become invalid after rotation (users must re-login). This is the desired outcome — surfaces stale tokens.
  • When production service is provisioned, use a SEPARATE openssl rand -hex 32 value (NOT the same as staging — feedback-render-env-vars-put.md-adjacent precaution)
Review
IMMIGR8-274
Persist ai_provider on evaluations table (closes Path 3a tagging gap)
Medium (P2)UnassignedCLAUDE
✓ Acceptance criteria
  • Add ai_provider varchar(64) column to evaluations table (apps/api/src/db/schema.ts)
  • Migration script apps/api/src/db/scripts/apply-immigr8-NNN-evaluations-ai-provider.ts (idempotent ALTER TABLE ... ADD COLUMN IF NOT EXISTS)
  • In submissions.ts /ai-analyze handler: pass result.providerName through to the evaluation insert/upsert
  • In submissions.ts /approve-ai-draft handler: read evaluation.aiProvider and pass to TrainingExamplesRepository.captureFromApproval
  • Backfill: existing evaluations get ai_provider = NULL (acceptable — they predate the adapter pattern)
Review
IMMIGR8-275
Path 3 decision gate: ML talent + budget + serving + dataset threshold
Medium (P2)UnassignedMANUAL
✓ Acceptance criteria
  • Jake records decisions on each of the 4 above in the Linear comment
  • If "go" on Decisions 1-3: post follow-up sub-tickets for the actual fine-tune sprint
  • If "wait": parent epic IMMIGR8-271 stays open until thresholds hit
Review
IMMIGR8-276
Backfill training_examples from historical approved evaluations (one-off script)
Medium (P2)UnassignedCLAUDE
✓ Acceptance criteria
  • Script apps/api/src/db/scripts/backfill-training-examples.ts
  • Reads all evaluations where is_ai_draft = false (approved / human-entered) AND there's at least one course_mapping
  • For each, calls TrainingExamplesRepository.buildSnapshot + insert (skipping any that already have a training_examples row to be idempotent)
  • Sets ai_provider = null for backfilled rows (we don't know which model produced the original — could be human-entered, IMMIGR8-118 auto-applied, or post-IMMIGR8-257 draft-then-approved)
  • Sets approved_at = evaluations.updatedAt (best approximation)
  • Logs counts: total inspected, backfilled, skipped (already exists), errors
  • Run on staging-db once, verify count, then on production-db when production lands
Review
IMMIGR8-283
Canada provincial drill-down (subdivision picker pattern, Ontario as prototype)
High (P1)UnassignedCLAUDE
✓ Acceptance criteria
  • Schema + migration applied; Canada provinces seeded
  • Canada submission with provinceId set saves + reads correctly
  • Submission for a country WITHOUT subdivisions (e.g., Mexico) doesn't show the picker (graceful no-op)
  • At least one Canadian province (Ontario, as the highest-volume) gets curriculum-rule overrides via CANADA_RULES.subdivisions\['ON'\]
  • Validation tests for Ontario via run-ai-analysis-on-submission.ts on a real ON student transcript
  • PDF + UI display the province alongside country
Review
IMMIGR8-298
Native in-product renewal management - HQ Renewals page + workflow
sandyMANUAL
P0 · Rewritten 2026-05-18 - native in-product Renewals page + workflow (no MD files)
✓ Acceptance criteria
  • HQ surface - new Renewals page in [hq.immigr8.net](<http://hq.immigr8.net>) (sidebar nav alongside Districts, Submissions, etc.). Lists every district whose subscription is approaching renewal (default: next 90 days) in a sortable table: District, plan tier, renewal date, days-until, owner, status, last-touch.
  • Renewal workflow status - per-district renewal record with status pipeline: Upcoming -> Outreach Sent -> Quote Sent -> In Negotiation -> Renewed / Churned / On Hold. Status transitions logged with timestamp + user.
  • Renewal detail page - for one district: contract value (current + projected), key contacts, renewal notes (timeline of comments), uploaded artifacts (signed agreements, quotes), next-action checkbox list. No MD file required - everything lives in the DB.
  • Owner assignment - each renewal has an assigned platform_team user (Presley by default for current cohort, configurable). Assignee gets notification when status changes or when 30/14/7-day reminders fire.
  • Reminders/cron - automated email + in-app notification at 90 / 60 / 30 / 14 / 7 days before renewal date if status is not yet Renewed or On Hold. Fires from existing notification infrastructure.
  • Reporting - HQ Dashboard tile: "Renewals at risk" count (upcoming in 60 days + status not yet Renewed), drill-down link to the page. Optional second tile: "Renewals YTD - won / churned / pending."
  • Trainable + repeatable - no Claude-specific format anywhere. Sandy + Presley can pick this up in 15 min by following sidebar -> Renewals -> click district -> edit fields. New hires onboard via standard HQ training, not a separate "how to edit MD files" doc.
🚧 Owner: Sandy + Presley · Copy `management/renewal-checklist-template.md` to a Google Doc or 1Password Document → populate the District Table with rows for districts up for renewal in the next 90 days → start Presley's Monday cadence.
Review
IMMIGR8-299
Add credits-earned ROI metric to product dashboards
UnassignedCLAUDE
Review
IMMIGR8-301
Draft requirements for transcript evaluation ordering; implement in new platform
UnassignedMANUALCLAUDE
Review
IMMIGR8-305
Add credits-awarded counter to evaluation detail; avoid X/Y phrasing
UnassignedCLAUDE
Review
IMMIGR8-306
Implement course editing: drag-and-drop, add/remove, course picker, recommended-course override
UnassignedCLAUDE
Review
IMMIGR8-307
Implement intake guardrails: USA confirmation, tooltips, citizenship disclaimer; add state-specific toasts
UnassignedCLAUDE
Review
IMMIGR8-308
Move dedupe to Settings; replace Accounts dedupe w/ + button; add table pagination (10/20/50/100)
UnassignedCLAUDE
Review
IMMIGR8-310
Fix evaluation PDFs: enable previews/regenerate; activate certificate logo at high quality
UnassignedCLAUDE
✓ Acceptance criteria
  • Preview button opens PDF inline in new tab (not download). Implementation: API supports ?inline=true query → sets Content-Disposition: inline. Client uses window.open(blobUrl, '_blank') when label is "Preview".
  • Download button still downloads when status is completed (preserved current behavior — Content-Disposition: attachment by default, a.click() on client).
  • Cert preview button continues to work — no change needed (already inline + window.open).
  • Certificate logo at the rendered 35mm × 35mm cert seal renders sharply — verified visually by opening a generated PDF.
  • Typecheck + build green.
Review
IMMIGR8-329
Fix HQ-branded favicon + manifest leaking onto customer-facing app surface
High (P1)UnassignedCLAUDE
✓ Acceptance criteria
  • apps/web/public/site.webmanifest "name" field updated from "Immigr8 HQ" to "Immigr8" (or whatever Jake prefers for the customer app label)
  • apps/web/public/site.webmanifest "short_name", "theme_color", "background_color" reviewed + updated to brand colors (currently #0a0a0b which is HQ's dark-mode background)
  • apps/web/public/favicon-192.png + favicon-512.png + favicon.png verified to match the brand-blue circle + white "8" design (regenerate from the SVG if mismatched — use macOS Preview or ImageMagick rsvg-convert)
  • apps/web/public/apple-touch-icon.png verified brand-correct (regenerate if mismatched)
  • hq/public/assets/favicon.svg replaced with a brand-aligned design (either the same brand-blue+white-8 as app, OR a clearly intentional HQ variant per Jake's call — currently it's just an orange dot which is neither)
  • Cache-bust: bump the favicon path with ?v=2026-05-12 query string in HTML refs OR add a <meta name="theme-color"> change to force browser revalidation (some users have stale favicons cached from prior visits)
  • Smoke test: install PWA on iOS Safari + Android Chrome from app.immigr8.net — confirm home-screen label says "Immigr8" not "Immigr8 HQ" + icon is brand-correct
  • Smoke test: visit hq.immigr8.net after deploy — confirm HQ favicon is brand-aligned
Review
IMMIGR8-330
Course editing: drag-and-drop reorder with display_order persistence
Medium (P2)UnassignedCLAUDE
✓ Acceptance criteria
  • Schema migration: add course_mappings.display_order INT NULL column (null = use default sort, integer = explicit position within grade-level)
  • Backend: PATCH /api/submissions/:id/mappings/reorder accepts { orderedIds: string[] } and writes display_order 0..N
  • Frontend: drag handle (GripVertical icon, lucide-react) on each row in GradeMappingTable
  • HTML5 native drag-and-drop (no new dep) — reordering within a grade-level table updates client state immediately + PATCHes the new order
  • Up/down arrow buttons as keyboard/touch fallback (a11y + mobile)
  • PDF renderer respects display_order when set, falls back to subject-category sort otherwise
  • Reorder is per-grade-level (can't drag a row from "9th" into "10th")
  • Unit tests for the reorder repo function + the sort comparator
Review
IMMIGR8-332
Phase 0: anonymized training-pair extraction layer (DPA-compliant)
Urgent (P0)UnassignedCLAUDE
✓ Acceptance criteria
  • Schema migration: new training_pairs table with the de-identified shape
Review
IMMIGR8-333
Auto-update dev.immigr8.net from Linear board (webhook → Render deploy hook)
Medium (P2)UnassignedCLAUDE
Review
IMMIGR8-335
Collapse dual-PEIMS in evaluation PDF + web
High (P1)UnassignedCLAUDE
P2 · Dual-PEIMS render fix shipped commit 325f8ee - Sandy spot-check pending
✓ Acceptance criteria
  • New shared helper formatCourseDisplay(usEquivalent, courseCode, foreignCourse) strips trailing 6-8 char codes (TWEDS digits + PES alphanumerics) from the label, then appends the canonical courseCode. Lives at packages/shared/src/format-course-label.ts, exported from @immigr8/shared.
  • apps/api/src/lib/generate-evaluation-pdf.ts:322 uses the helper so every Course cell shows exactly one PEIMS reference.
  • apps/web/src/pages/SubmissionDetail.tsx uses the helper in both CoursePicker instances (AI-draft row + manual-entry row) AND in the fmtCourseLabel() helper used by other render paths on the page.
  • Typecheck pass + build pass on shared, api, web.
  • Sandy spot-checks one of the previously-doubled records (e.g. Eder Gael's English II row) - Course column now shows English II ... - #<one-code>.
Review
IMMIGR8-336
PDF portrait/landscape orientation toggle on Preview + Download
Medium (P2)UnassignedCLAUDE
P3 · PDF portrait/landscape toggle shipped
Review
IMMIGR8-337
EOC matcher: code-anchor + regex fallback (resolves ESOL + Since-1877 misses)
High (P1)UnassignedCLAUDE
P2 · EOC matcher 3-tier upgrade (code-anchor + regex)
Review
IMMIGR8-338
Country detail UX: standardized sections + inline NCAA (no dropdown)
High (P1)UnassignedCLAUDE
P2 · Country detail UX standardized + inline NCAA
Review
IMMIGR8-339
clients.immigr8.net portal + Fort Worth ISD proposal scaffold
High (P1)UnassignedCLAUDE
P2 · clients.immigr8.net live - awaiting Jake to send Fort Worth ISD deal flow data
Review
IMMIGR8-345
dev.immigr8.net live refresh: JSON endpoint + client render + manual Refresh button
High (P1)UnassignedCLAUDE
✓ Acceptance criteria
  • GET /api/briefs/standup.json endpoint on api-staging with chosen auth approach
  • Briefing index.html becomes a thin shell that fetches + renders client-side
  • "Refresh from Linear" button works and re-renders cards
  • "Last refreshed HH:MM" timestamp visible
  • Historical archive cron snapshots HTML at 0700 CT to archive/{date}.html
  • YTD page reads from archive folder (not main current briefing)
  • Sandy can open the page midweek and see today's Linear state without anyone touching git
Review
IMMIGR8-347
Dasom Kim transcript: correct mis-aligned PEIMS codes (grade 10 + grade 8)
High (P1)UnassignedCLAUDE
✓ Acceptance criteria
  • Grade 10 first Algebra II row: course_code updated from 03220200 to 03100600
  • Grade 10 second row corrected: us_equivalent + course_code now reflect "Mathematical Models with Applications" / 03102400
  • Grade 8 Algebra I row: us_equivalent cleaned from "Geometry - #03100700" to "Algebra I"
  • Sandy verifies the rendered PDF (preview or download) for Dasom Kim shows the corrected rows
  • Follow-up IMMIGR8-343 filed for the systemic validator (course_code ↔ us_equivalent ↔ TWEDS title alignment)
Review
IMMIGR8-349
PDF page-count consolidation (compact continuation header + tighter footer)
High (P1)UnassignedCLAUDE
✓ Acceptance criteria
  • New addCompactHeader() renders a 12mm continuation band on pages 2+ (logo + student-name/country + "HS Credit Evaluation (continued)" + blue rule). First page keeps the full cover header for the identifying info.
  • Footer disclaimer compressed to one italic line ("Districts retain final discretion to accept or reject this evaluation. Immigr8 is a private credential-evaluation agency."). FOOTER_HEIGHT drops from 25mm to 14mm.
  • Grade section title bar 8mm -> 7mm; subtotal bar 7mm -> 6mm; inter-section gap 4mm -> 2mm.
  • Table body cellPadding tightened (1mm vertical) so rows render \~4.2mm vs prior \~4.6mm.
  • estimateGradeSectionHeight() updated to match real measurements - no more premature page breaks from a stale conservative estimate.
  • Typecheck pass.
  • Sandy/Jake verify Dasom Kim's landscape PDF now renders in 2 pages (or 3 max with summary), no obvious whitespace.
Review
IMMIGR8-350
Standup unification round 2: branding + SSR live + clean URLs + index depth
High (P1)UnassignedCLAUDE
✓ Acceptance criteria
  • All 3 pages share the same full Immigr8 logo via the gen-standup.ts nav() helper (data URI; no separate favicon usage)
  • live.html is server-side rendered by the generator with the current week's content baked in. JS still runs after page load to fetch fresh data and replace #root, but first paint is real content - no more flash
  • Render route rewrites map /this-week -> /live.html and /analytics -> /analytics.html. Nav links updated to use the clean paths
  • Index drops the 5-status band; replaces with: Weekly Throughput Trend (12-week bars), Recently Completed (last 12), In Review Awaiting Verification (top 10), Blocked (top 10), plus the existing "Where to go" + Weekly Briefings archive
  • Analytics drops the 5-status band; keeps the YTD slices (month / priority / label / assignee / recently shipped)
Review
Discussion:Click Done on anything verified; flag anything still being tested.

Done— 7 tickets

~2 min
IMMIGR8-235
Standardized Subject Area Ordering for Transcript Evaluation Outputs
High (P1)sandyCLAUDE
P2 · Sandy approved Apr 29 "working accurately" - ready for Done click
Done
IMMIGR8-237
Transcript Evaluation Audit Trail with Timestamping, Specialist Attribution, and Version History
Low (P3)sandyCLAUDE
Done
IMMIGR8-271
AI vendor independence (Path 2) + fine-tuning prep (Path 3) — parent epic
High (P1)UnassignedCLAUDE
Done
IMMIGR8-284
Countries tab: global heat map / aggregate-stats view (top hitters at a glance)
High (P1)UnassignedCLAUDE
Done
IMMIGR8-285
PDF: pack evaluation onto 1-2 pages (was 5-7+) — stop forced page-per-grade
High (P1)UnassignedCLAUDE
Done
IMMIGR8-327
Draft FWISD deal-flow summary (org, COI, budget); Slack Jake
sandyCLAUDE
P0 · Sandy completed Sun May 18 - moved to In Review
Done
IMMIGR8-328
Email Jake booth design assets (logos, refs); then Jake requests design, Jake/Sandra approve
sandyMANUAL
P0 · Sandy completed Sun May 18 - moved to In Review
Done
Discussion:Anything here worth flagging as a launched milestone?
Suggested 30-minute agenda
  1. 5In Review · close-outs: anything Sandy verified → Done
  2. 5In Review · verification: quick yes/no on shipped items
  3. 8★ Mexico alpha gate (IMMIGR8-257): Diego & Eder drafts. Validated? Highest-leverage item.
  4. 7In Progress: 278 spike scope, 334 student dedup plan
  5. 3Done this week: celebrate shipped items + flag launched milestones
  6. 2Wrap + next-week preview
SSR'd 2026-05-20T17:58:22.733Z · auto-refreshes from /api/briefs · IMMIGR8-341