Work log: Scholexis — March 22, 2026
What shipped today
This was a massive hardening and infrastructure session. Two full scout cycles surfaced 10 issues across security, error handling, performance, dead code, UX, and testing — all 10 were executed and merged within the day. The session also established the project’s first test infrastructure from scratch.
Security hardening was the highest-impact work. An IDOR vulnerability in cascading delete operations (#176) was fixed — deleteAcademicYear() and deleteTerm() were deleting child records without verifying ownership, meaning an attacker who knew a valid ID could delete another user’s data. Ownership checks now happen before any cascading operations, and all cascading deletes are wrapped in database transactions. The signup action was also hardened: the role field now validates against a whitelist (#191), preventing privilege escalation via crafted requests. Rate limiting (#194) was added to both login and signup endpoints using an in-memory sliding window rate limiter — 5 login attempts and 3 signup attempts per email per 15 minutes.
Error handling received a systematic overhaul (#177). All server actions across 7 files now wrap database operations in try/catch and return structured error objects instead of throwing. All 12 client components that call server actions now check return values for validation errors and use isRedirectError() to distinguish Next.js redirect throws from real failures. Users now see toast notifications on actual errors instead of silent failures.
Test infrastructure was built from zero (#185, #186, #187). Vitest was installed and configured with jsdom environment and @/ path aliases. A CI job (test_web) now runs tests on every PR. Validation schemas received 52 unit tests covering all 9 Zod schemas. The generateWeeks() function was extracted from the server action into a pure testable module (lib/week-utils.ts) and covered with 10 tests. The project went from 0 tests to 75.
Performance improvements included adding 5 missing database indexes (#178) on frequently-queried columns and parallelizing getDashboardData() (#193) from 12+ sequential queries to a single Promise.all() batch — expected ~10x latency improvement on the dashboard.
UX polish replaced all 5 browser confirm() dialogs with styled ConfirmDialog components (#192) using @base-ui/react AlertDialog primitives. Language was updated to be shame-free (“Remove course” instead of “Delete this course and all its meetings?”). Dead code cleanup (#179) removed ~500 lines of unused components, exports, and functions. The AUTH_URL port mismatch (#195) was fixed locally.
Completed
- #176 — Fix IDOR vulnerability in cascading delete operations (PR #181)
- #177 — Fix silent error swallowing in server actions and forms (PR #182)
- #178 — Add missing database indexes for query performance (PR #183)
- #179 — Remove dead code — unused components, schema tables, and functions (PR #184)
- #180 — Set up test framework and write critical path tests (decomposed into #185-187)
- #185 — Set up Vitest and add test script to CI (PR #188)
- #186 — Test validation schemas in lib/validations.ts (PR #189)
- #187 — Test generateWeeks date arithmetic and week alternation (PR #190)
- #191 — Validate role field in signup against whitelist (PR #196)
- #192 — Replace browser confirm() with styled confirmation dialogs (PR #197)
- #193 — Parallelize getDashboardData() queries and add error handling (PR #198)
- #194 — Add rate limiting to login and signup endpoints (PR #199)
- #195 — Fix AUTH_URL port mismatch in .env (local-only fix, .env is gitignored)
Release progress
- Next.js port: 88/90 closed (2 open, both
needs-clarification) - v1.0: 6/6 closed
Carry-over
- #64 (production deployment pipeline) and #65 (data migration script) remain open with
needs-clarification— both awaiting human input on whether production users exist and which hosting platform to use. - Account enumeration via signup error messages (“email already exists”) — noted but not filed as an issue yet.
- Missing loading states on 20+ dashboard pages — lower priority, not yet tracked.
- No pagination on list pages — noted but not urgent at current data volumes.
Risks
- Rate limiter is in-memory — resets on server restart and doesn’t work across multiple instances. Acceptable for pre-launch but must be upgraded to Redis/Upstash before scaling.
- The
ConfirmDialogcomponent uses @base-ui/react AlertDialog which may have different behavior than expected with Next.js server components — needs browser testing.
Flags and watch-outs
- Test count is now 75 (5 test files). All pure logic — no database-mocked tests yet. Auth flows and server action ownership checks remain untested.
- The
getDashboardData()function now returns empty defaults on any error rather than crashing. This is safer but could mask database connectivity issues in production — consider adding error logging before the catch returns. - The
computeWeeks()extraction changedterms/actions.ts—createTerm()now callscomputeWeeks()+ inline DB insert instead of the oldgenerateWeeks()private function. Behavioral parity assumed but not browser-tested.
Why customer tools are organized wrong
This article reveals a fundamental flaw in how customer support tools are designed—organizing by interaction type instead of by customer—and explains why this fragmentation wastes time and obscures the full picture you need to help users effectively.
Infrastructure shapes thought
The tools you build determine what kinds of thinking become possible. On infrastructure, friction, and building deliberately for thought rather than just throughput.
Server-side dashboard architecture: Why moving data fetching off the browser changes everything
How choosing server-side rendering solved security, CORS, and credential management problems I didn't know I had.
The work of being available now
A book on AI, judgment, and staying human at work.
The practice of work in progress
Practical essays on how work actually gets done.
When your agents start breaking each other's code
Two agents modified the same file independently and created database locks. The fleet hit 135 issues in one day — and the coordination problem that comes with it.
The removal tax
The most productive thing you can do with a product is take features away. Eighty-nine issues closed across eight projects, and the hardest lesson came from a pipeline that ran perfectly and produced nothing.
The product changed its mind
A product pivoted its entire philosophy mid-session — from 'here's your list' to 'here's your next thing.' The code shipped in the same conversation as the idea. That's not iteration. That's something else.