How I Built NeuroSpace: An AI Productivity Platform That Actually Thinks With You
Most productivity apps are passive. They hold your tasks and wait. You still have to decide what to work on, estimate how long things take, and figure out why you keep missing deadlines. The cognitive overhead of managing a productivity system often exceeds the benefit. I wanted to answer one question: what would a productivity tool look like if it actually thought alongside you? That question became NeuroSpace. What I Built NeuroSpace is a full-stack AI productivity platform. The core loop is simple: plan your work with AI, execute it with a focus timer, review your patterns with data analysis, and get warned before you fall behind. Live: neurospace-zr2n.vercel.app Code: github.com/naimakader/Neurospace The Stack and Why I chose Next.js 15 App Router because server components reduce the client bundle and API routes keep server logic close to the UI that uses it. TypeScript gave me shared types between API responses and UI state - the source of truth lives in one place instead of being duplicated. For auth I used Clerk. Production-grade authentication without building session management from scratch. JWT templates let Clerk tokens authenticate Supabase requests directly. For the database I chose Supabase with PostgreSQL. Row-level security scopes every query to the authenticated user automatically. A JSONB column stores session snapshots efficiently - more on why that matters below. For AI I used OpenAI GPT-4o-mini. Best price-to-quality ratio for structured JSON generation and conversational responses. The Hardest Engineering Decision: Session Snapshots The naive approach to loading state: on every page load, fetch all tasks and reconstruct the board. The problem is that undo/redo history is lost, column order is lost, and archived tasks disappear. My approach: after every mutation, save the complete state as { tasks, archived } to a task_history table using an upsert on user_id. One row per user, always current. On load, I try the snapshot first. It is the complete, ordered, correct state. I fall back to the tasks table only if no snapshot exists. This also makes undo/redo persistent across page refreshes. After undo, the restored state saves immediately as the new snapshot and re-inserts into the tasks table so the database matches memory. Why useRef for Undo/Redo Undo/redo stacks do not cause re-renders - they only matter at the moment of undo. Using useState would trigger unnecessary renders on every single mutation registration. I used useRef for both the past and future stacks, and structuredClone to create deep copies that handle Date objects correctly - safer than JSON.parse/stringify. Why Midnight Archiving Instead of 24 Hours A task completed at 11pm would still appear in the Done column the next morning under a 24-hour rule. The correct boundary is midnight in local time - not UTC, which would shift by the user's timezone offset. This is a small detail that completely changes the user experience. Getting it wrong makes the app feel broken even when the code is technically correct. The Bugs That Taught Me the Most Silent persistence failure. Tasks were saving locally and appearing on the board but disappearing on refresh. No errors in the console. Every POST request returned 400 silently. The API was validating the wrong data shape - the snapshot structure had changed when I added archive support but the API never got updated. Lesson: when two systems share a data contract, define the type once and import it in both places. Undo across refresh. Undo worked perfectly in the same session. After refresh the undone delete came back. The task had been physically deleted from Supabase - the snapshot saved the restored state but the tasks table did not have the row. Fix: after every undo/redo, sync the actual database state to match restored state. Hydration mismatch. Math.random() was called at module level to generate a tab identifier. Server rendered one value, client hydrated with a different value. React threw a hydration error. Fix: move random generation inside a useRef that only initializes on the client. Case sensitivity on Linux. The app worked perfectly on Windows locally. Vercel build failed with Module not found. The file was saved as focusMode.tsx - Windows is case-insensitive, Linux is not. Fix: git mv to rename the file. Lesson: always match import casing to file casing exactly. AI Integration: Graceful Degradation Every AI endpoint has a local fallback. The app works without OpenAI credits. It gets smarter when you have them. The planner also adapts to the user's energy level before calling GPT. A mood multiplier adjusts session lengths - low energy gets 60% of normal session length, high energy gets 130%. GPT's time allocations match actual capacity instead of assuming everyone works the same way. Results - Clerk JWT to Supabase auth: under 100ms per request - Full state restoration: single JSONB query - AI plan generation: 1-3 seconds, instant with local fallback - Undo/redo: O(1) push/pop with ref stacks, persistent across refresh - First load JS: 287kb What I Would Do Differently Define API contracts first. I built the provider and API routes in parallel and they drifted. In a real team you write the OpenAPI spec first and generate types from it. Test the error paths, not just the happy path. The silent persistence failure would have been caught immediately by a test asserting the snapshot shape. Use a type-safe API client. Raw fetch with manual JSON parsing requires manual type assertions everywhere. tRPC would have made the shape mismatch impossible. Never assume case-insensitivity. Build and test on Linux from day one. Final Thought The hardest part of building something complex is not any single technical problem. It is keeping the whole system coherent while solving them one by one. NeuroSpace taught me that more than any tutorial ever could. Built by Naima Kader Portfolio: https://portfolio-seven-beryl-29.vercel.app/ GitHub: github.com/naimakader/Neurospace Live: neurospace-zr2n.vercel.app Top comments (0)
Comments
No comments yet. Start the discussion.