Rawdocumentationfromthetrenches
Technical deep-dives, architectural decisions, and lessons from the process of building real things.
Entries
Categories
Active Since
Transmissions
Field reports
Shipping the Permission Graph
Fyboard's permission system needed field-level access control across 8 modules, 4 roles, and tenant isolation — all resolved at request time.
Permission checks were taking 120ms+ per request under load. Every API call triggered a cascading set of policy evaluations against the role hierarchy.
We were checking permissions top-down: for each field, walk the role tree to see if the user has access. O(fields × depth) on every request.
Inverted the lookup. On authentication, pre-compute the full accessible field set per role into a Redis bitmap. Permission checks became a single bitwise AND — O(1).
Moved from runtime policy evaluation to a pre-computed permission cache with invalidation on role/policy mutations. Added a migration to backfill bitmaps for existing roles.
This pattern generalizes. Any permission system that checks per-resource can be flipped into a pre-computed set. We now resolve field-level access in under 5ms at any scale.
Why I Rebuilt the Task Engine Three Times
Fyboard's core is a task engine. It needs to model statuses, transitions, dependencies, recurring schedules, and integrations — without becoming a maintenance nightmare.
Version 1 was a CRUD app with status strings. It couldn't express 'blocked by' or 'recurs every Tuesday'. Version 2 used a full state machine library — but the abstraction was heavier than the feature.
I was over-indexing on generality. The state machine handled every theoretical transition, but 90% of tasks only needed 3 states. The abstraction cost more than the flexibility it provided.
Version 3: a thin state machine for the 3 common states (todo/active/done) with an escape hatch to a full DAG for complex workflows. Two layers, one interface.
Vector Embeddings for Context Memory
Building an AI memory layer that persists context across conversations. The model needs to recall previous decisions, user preferences, and ongoing project state.
Naive full-context injection hit the token limit after 3 conversations. Summarization lost critical details. The model kept re-asking questions it had already resolved.
Text summarization is lossy — it preserves narrative but drops structured facts. 'The user prefers TypeScript over Python' gets averaged out of a summary about a coding session.
Built a retrieval pipeline using Pinecone vector embeddings. Each conversation turn gets embedded and scored on three axes: semantic relevance, temporal recency, and declared importance.
OKLCh: The Color Space That Changed Everything
Every project had inconsistent color palettes. Generating shades from HSL produced muddy mid-tones and unpredictable contrast ratios between steps.
HSL(210, 80%, 50%) and HSL(60, 80%, 50%) have the same 'lightness' but wildly different perceived brightness. Palette generation was trial-and-error, not systematic.
HSL lightness isn't perceptually uniform. A 10% lightness step at hue 60 (yellow) looks nothing like a 10% step at hue 240 (blue). The math doesn't match human vision.
Switched the entire design system to OKLCh. Defined all colors as oklch(L C H) with consistent lightness steps (0.15 increments). Accent, muted, dim — all derived from the same L/C curves.
Scroll-Driven Animations Without the Jank
This site needed layered scroll animations — parallax backgrounds, reveal-on-scroll content, and choreographed section transitions. All at 60fps on mobile.
Initial implementation used scroll event listeners with direct DOM manipulation. Jittery on mobile, layout thrashing on resize, and impossible to coordinate timing across sections.
Scroll events fire on the main thread. Any work in the handler blocks paint. Combined with getBoundingClientRect calls (forcing layout recalc), every frame was a full reflow.
Replaced everything with Framer Motion's useScroll + useTransform pipeline. Scroll progress drives motion values through spring transforms — all computed off-thread via WAAPI.
Modular Architecture: Lessons from Fyboard
Fyboard grew from a task manager to a full productivity suite. Every new feature (notes, calendar, integrations) was coupling deeper into the core schema.
Adding a calendar feature required modifying the task table, the API router, the permission system, and the UI shell. A 'simple' feature touched 40+ files.
Monolithic coupling. The task schema had become a god table with optional columns for every feature. The API router mixed concerns. No feature boundary existed.
Extracted each feature into a self-contained module: own schema, own API routes, own UI components, own permission definitions. The core provides a module registry and shared auth — nothing else.
Building a Real-Time Workflow Engine
Fyboard needed to automate multi-step processes: 'when a task moves to Done, update the project progress, notify the team, and trigger the next task in the pipeline.'
Chained event handlers created invisible execution paths. Errors in step 3 left steps 1-2 committed. No retry logic. No way to inspect the current state of an in-flight workflow.
Event-driven chains without explicit state. Each handler knew about the next step but had no concept of the overall workflow, its progress, or how to recover from partial failure.
Replaced event chains with XState state machines for workflow definitions and BullMQ for durable job execution. Each workflow step is a job with explicit success/failure transitions.
Canvas Particle Systems at 60fps
Needed an interactive background for this site — thousands of particles with physics-based mouse repulsion, connection lines between neighbors, and smooth 60fps on all devices.
Naive implementation with Array.forEach and distance checks between all particle pairs ran at 8fps with 5,000 particles. GC pauses caused visible stutters every 2-3 seconds.
O(n²) neighbor lookups with no spatial indexing. Plus, creating new objects every frame for position vectors triggered garbage collection storms.
Implemented spatial hashing (divide canvas into grid cells, only check neighbors in adjacent cells). Object pooling for all vector math — zero allocations per frame. Delta timing for frame-rate independence.
“The best documentation is written while building, not after.”