Building software, breaking things, and writing down what I learned.


Tag: AI

  • The AI Wasn’t the Hard Part

    The AI Wasn’t the Hard Part

    Building an AI-powered feature, navigating frontend conventions, and wrestling with a codebase that wouldn’t sit still.

    I recently worked on an AI-powered feature that takes an insertion order (IO) PDF, extracts campaign information, and uses it to populate a draft in our DSP campaign builder.

    Sounds like the AI would be the complicated part, right?

    Not so much.

    I’d done some prompt engineering before, so I had a decent starting point. The MCP tools I needed were already built, although a few API adjustments were necessary to get the right data. Connecting the agent to our internal assistant infrastructure was new territory for me, but other engineers in my pod had done similar work. I had existing implementations to reference and people I could turn to when I needed help.

    Then I got to the frontend.

    Between navigating component conventions, managing asynchronous processing and form state, and discovering that another feature flag was about to rearrange the campaign builder’s fields and step order, I had plenty to keep me occupied.

    Don’t get me wrong. There was still engineering involved in the AI integration. But the challenges that took the most effort weren’t really AI problems at all.

    Apparently, adding AI to an application doesn’t exempt you from the usual software engineering headaches.

    Getting the Data Where It Belongs

    Before diving into implementation, I worked through the requirements and architecture using internal AI-assisted planning tools. I reviewed the proposed approach and documentation before getting feedback from my product manager and engineering manager.

    The basic workflow sounded simple enough: upload a PDF, extract the relevant campaign information, and populate the appropriate fields.

    Except there were a few catches.

    For starters, extraction wasn’t synchronous. Once a user uploaded a PDF, the browser extracted its text and sent it for processing. The frontend then needed to wait for the assistant to finish and retrieve the results.

    That meant managing loading states, polling for responses, handling timeouts, and accounting for users who might upload another document before the first one finished processing.

    Consider what happens when someone uploads one IO, changes their mind, and uploads another before the first extraction completes.

    The application can’t just accept whichever response comes back last. Otherwise, you risk populating the campaign with information from a document the user has already replaced.

    In my implementation, a new upload stops polling for the previous one, preventing its results from being applied to the current draft. Failed or timed-out extractions also need to be communicated to the user rather than leaving the interface stuck in a loading state.

    Then there was the question of what to do with the extracted data.

    The campaign builder was already a multi-step form with its own validation and save behavior. I couldn’t just take the extracted values, shove them into the form, and hope for the best.

    The user needed to review and approve the extracted information before it was applied. Even then, not every value belonged in the form immediately.

    Some fields could be populated right away, while others needed to be staged until the user reached the appropriate step.

    Re-uploading a document introduced another complication. What if the user had already reviewed or modified part of the campaign? The new extraction couldn’t simply overwrite everything.

    That meant distinguishing between information that could be replaced and information that needed to be preserved, while making sure failed operations didn’t leave the form in an inconsistent state.

    Cursor helped considerably with the implementation, particularly when working through unfamiliar parts of the codebase and some of these state management challenges. But there was still plenty of reviewing, testing, and figuring out how everything needed to fit together.

    Because apparently extracting the data was only half the battle. Getting it into the right place without breaking something else was where things got interesting.

    The Front End Had Other Plans

    I’ve worked with Figma designs plenty of times, so I wasn’t expecting the UI to be my biggest headache.

    That was optimistic.

    The issue wasn’t necessarily that the designs were bad. It was that they left a lot of questions unanswered once I started translating them into actual components.

    Which components were intended? Should I use something straight out of Ant Design or one of our internally customized versions? What were the conventions for styling and handling different processing states?

    And seriously, what color was I supposed to use?

    There were also existing frontend patterns to consider. I wasn’t building a standalone application where I could make every decision from scratch. This feature needed to look and behave like it belonged in the campaign builder.

    I spent a fair amount of time working through those questions with the frontend SME in my pod. We compared options against existing conventions, figured out how to handle ambiguous components and statuses, and brought in the design team when we needed additional clarification.

    It wasn’t particularly glamorous work, but it mattered. The difference between a feature that technically works and one that feels like a natural part of an application often comes down to those little decisions.

    And apparently, figuring out which alert color to use can take more effort than connecting an AI agent to an API.

    Feature Flags: Because One Wasn’t Enough

    Just when I thought I had a pretty good handle on the implementation, I learned about another feature flag that was going to change the campaign builder’s fields and the order of its steps.

    Information that would have been really useful a little earlier.

    I’d already built a substantial portion of the ingestion workflow around the existing configuration. Now I needed to account for an updated version of the same application.

    Fortunately, I didn’t have to start over.

    The ingestion feature had its own feature flag, and the campaign builder changes were controlled by a separate one.

    Rather than tie my implementation to the upcoming changes, I kept what I’d built and made adjustments so the ingestion workflow could handle either version of the campaign builder.

    That meant accounting for differences in the available fields and where extracted information needed to be populated, depending on which configuration was active.

    Was it annoying? Absolutely.

    But it also reinforced something important about working in a shared codebase: you’re rarely developing against a static application.

    Other features are being built, existing functionality is changing, and sometimes those changes overlap with your own work in ways you didn’t anticipate.

    You can plan for the requirements you know about. The trick is being able to adapt when you discover the ones you didn’t.

    So, What Was Actually Hard?

    Looking back, the most challenging parts of this project weren’t the ones I initially associated with building an AI agent.

    The AI integrations came together relatively smoothly because I had existing tools, examples, and teammates to lean on.

    The bigger challenges were fitting the extraction workflow into an established application, managing state without disrupting existing behavior, navigating frontend conventions, and adapting to changes I hadn’t known were coming.

    None of that is particularly groundbreaking. It’s just software engineering.

    And I think that’s my biggest takeaway from this project.

    For all the excitement surrounding AI development, the fundamentals haven’t really changed. You still need to understand the systems you’re working with, account for things going wrong, and occasionally ask someone who knows a particular corner of the codebase better than you do.

    AI-assisted tools can make development faster, but they don’t make those decisions for you. At least, not decisions you should blindly trust.

    I went into this project expecting to spend most of my time figuring out the AI components. Instead, I spent a surprising amount of it dealing with frontend conventions, existing application behavior, and feature flags.

    Would I do it again? Absolutely.

    Would I ask a few more questions about the frontend before getting started?

    Also absolutely.

  • Building a Sunsama Clone: Part 1 – The Foundation

    Building a Sunsama Clone: Part 1 – The Foundation

    Or: How I Spent 3 Hours Avoiding My Actual To-Do List by Building a Better To-Do List

    This is part 1 of an ongoing series where I document building a production-ready task management app from scratch. Follow along for the technical deep dives, architectural decisions, and inevitable “well, that didn’t work” moments.

    The Problem (AKA My Life)

    Look, I’m a senior software engineer. I can architect distributed systems, debug race conditions in my sleep, and explain monads to junior devs without making them cry. But ask me to remember to buy groceries? Absolute chaos.

    I’ve tried everything. Notion (too many features), Todoist (too simple), bullet journaling (let’s not talk about it), and those fancy productivity apps that cost more than my monthly coffee budget. Then I discovered Sunsama and thought, “This is it! Time blocking! Daily planning! This will fix me!”

    It did not fix me. But it did inspire me to spend a Sunday building my own version, which is honestly the most on-brand thing I’ve ever done.

    The Stack (Let’s Talk Architecture)

    Frontend: React 18 + TypeScript + Vite

    Why React 18? Honestly, muscle memory. But also: concurrent rendering, automatic batching, and Suspense support that I’m not using yet but plan to. The new useId hook is going to be clutch when I build the drag-and-drop interface.

    Why TypeScript? Because I like sleeping at night. Also because catching “Property ‘title’ does not exist on type ‘Task’” at build time instead of in production is worth the extra keystrokes.

    Why Vite? Have you seen how fast it is? I hit save and the page updates before my eyes can refocus. Coming from Create React App, this is like going from dial-up to fiber. Plus, the default config just works. No ejecting, no rewiring, no webpack.config.js that looks like ancient runes.

    // vite.config.ts - that's it, that's the whole config
    import { defineConfig } from 'vite'
    import react from '@vitejs/plugin-react'
    export default defineConfig({
      plugins: [react()],
    })
    

    Frontend State Management: React Query + Zustand

    Here’s where it gets interesting. I’m using two state management solutions, and before you @ me, hear me out:

    React Query for server state – anything that comes from the API. Tasks, users (eventually), all of it. React Query handles caching, refetching, optimistic updates, the works.

    Zustand for client state – UI state that doesn’t touch the server. Think: “is the sidebar open?” or “which filter is active?”. I haven’t needed it much yet, but it’s installed and ready for when I do.

    The key insight: server state and client state are fundamentally different problems. Server state is async, can be stale, needs caching. Client state is synchronous, always fresh, no caching needed. Using different tools for different jobs = chef’s kiss.

    Backend: Node.js + Express + TypeScript + Prisma

    Express in 2025? Yes. Fight me. It’s boring, but it works, everyone knows it. I don’t need GraphQL subscriptions for a task app. RESTful routes are fine.

    Why Prisma? Three reasons:

    1. Type safety end-to-end (TypeScript on both sides talking to the same schema)
    2. Migrations that don’t make me cry
    3. This query syntax:
    const tasks = await prisma.task.findMany({
        where: { 
            userId,
            completed: false,
        },
        include: {
            user: true  // Automatic joins!
        },
        orderBy: { createdAt: 'desc' },
    });

    Compare that to writing raw SQL with string interpolation. Yeah, I’ll take the ORM.

    PostgreSQL via Supabase – I wanted Postgres (because it’s solid and has proper data types), but I didn’t want to run a database locally. Supabase gives me:

    • Hosted Postgres
    • Connection pooling
    • Automatic backups
    • A nice dashboard
    • Free tier that’s actually usable

    Coming in future posts: I’ll probably regret not using their auth system and will migrate to it eventually.

    The Monorepo Setup

    sunsama-clone/
    ├── client/          # React app
    ├── server/          # Express API
    └── package.json     # Root workspace config

    Using npm workspaces (not Lerna, not Turborepo, just npm) because:

    • It’s built into npm 7+
    • Dependencies hoist automatically
    • Scripts are easy: npm run dev --workspace=client
    • No extra configuration needed

    The root package.json just has:

    {
      "workspaces": ["client", "server"]
    }

    That’s it. Magic. Dependencies get installed to the root node_modules, each workspace has its own package.json for its specific deps. Clean, simple, works.

    The Build: A Series of Unfortunate Events

    Chapter 1: Docker, Why Have You Forsaken Me?

    Started with grand plans for a local PostgreSQL setup in Docker. The dream: docker-compose up and boom, perfect dev environment.

    Here’s what my docker-compose.yml looked like:

    version: '3.8'
    services:
      postgres:
        image: postgres:16
        environment:
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: sunsama_clone
        ports:
          - "5432:5432"

    Looks good, right? WRONG.

    Prisma refused to connect. Every error message was some variation of:

    • P1010: User 'postgres' does not exist
    • ECONNREFUSED
    • password authentication failed
    • the pain, the pain of it all

    Things I tried:

    1. Different connection strings (at least 15 variations)
    2. Adding a shadow database URL (Prisma’s docs suggested it)
    3. Waiting longer for Postgres to initialize (maybe it just needed time?)
    4. Checking Postgres logs (they said everything was fine, lies)
    5. Googling increasingly desperate phrases
    6. Questioning my career choices

    Here’s what I think was happening: Prisma was trying to connect before Postgres finished initializing, even with healthchecks. Or maybe there was a network issue between containers. Or maybe Docker was gaslighting me.

    The pragmatic decision: After dealing with this for entirely too long, I opted for Supabase, got a connection string, and it worked in 30 seconds.

    # The magic string that actually worked
    DATABASE_URL="postgresql://postgres:[password]@db.[project].supabase.co:6543/postgres?pgbouncer=true"
    DIRECT_URL="postgresql://postgres:[password]@db.[project].supabase.co:5432/postgres"
    

    Why this was the right call:

    • Dev/prod parity (I’ll use Supabase in production anyway)
    • No local Docker containers eating my battery
    • Connection pooling built-in
    • Automatic backups
    • I can access the database from anywhere

    Lesson learned: Sometimes “works on my machine” isn’t the goal. Sometimes “works, period” is enough. I’ll tackle Docker setup in a future post when I actually need local development isolation. For now, pragmatism wins.

    Coming in Part 2: I’ll probably set up Docker properly because I can’t help myself.

    Chapter 2: Tailwind v4 – A New Hope (But Different)

    Tailwind v4 dropped and changed… everything? Here’s what’s different:

    The Old Way (v3):

    // tailwind.config.js
    module.exports = {
      content: ['./src/**/*.{js,ts,jsx,tsx}'],
      theme: { extend: {} },
      plugins: [],
    }

    The New Way (v4):

    // postcss.config.js
    export default {
        plugins: {
            '@tailwindcss/postcss': {},
        },
    }

    And in your CSS:

    /* index.css */
    @import "tailwindcss";
    

    That’s it. No config file. No content paths. No theme object. It just… works.

    How it works: Tailwind v4 uses a new engine that scans your files automatically. It’s faster, the bundle is smaller, and the API is cleaner. The tradeoff? Breaking changes if you’re migrating from v3.

    The gotcha I hit: I tried to use @tailwindcss/forms plugin. Doesn’t exist in v4 yet. Had to style form elements manually, which honestly wasn’t terrible:

    input, textarea {
        @apply px-3 py-2 border border-gray-300 rounded-md;
        @apply focus:ring-2 focus:ring-blue-500 focus:border-blue-500;
    }

    Verdict: Once you get past the initial “wait, where’s my config file?” panic, it’s actually really nice. Less configuration, faster builds, same utility-first goodness.

    Coming in a future post: Custom design system with Tailwind v4’s new theming approach.

    Chapter 3: npm Workspaces Are Actually Cool?

    I set up a monorepo because I wanted client and server in the same repo without going full Turborepo mode. npm workspaces just… worked? Dependencies hoisted to the root, proper isolation, life was good.

    I was waiting for something to break. Nothing broke. This never happens. I’m still suspicious.

    Chapter 4: GitHub

    Created the repo and pushed my code. Self-explanatory. Took 2 minutes. Was this worth a chapter? Probably not.

    What Actually Works (The Good Part!)

    After all that setup chaos, I ended up with a fully functional CRUD app in about 3 hours total:

    ✅ Create tasks with titles, descriptions, and due dates
    ✅ Check off tasks (so satisfying)
    ✅ Delete tasks (even more satisfying)
    ✅ Real-time updates thanks to React Query
    ✅ Clean UI that doesn’t make my eyes bleed
    ✅ Mobile responsive

    The app works! I can add tasks! I can finish them! I won’t actually use it to complete tasks because that would require discipline, but technically I could.

    The Code: Deep Dives Into What Actually Works

    React Query: The Secret Sauce

    React Query handles all the annoying parts of data fetching. Here’s the full setup:

    // main.tsx - Query client configuration
    const queryClient = new QueryClient({
        defaultOptions: {
            queries: {
                refetchOnWindowFocus: false,  // Don't refetch when I tab back
                retry: 1,                      // Try twice, then give up
                staleTime: 5 * 60 * 1000,     // Data fresh for 5 minutes
            },
        },
    })

    Why these settings?

    • refetchOnWindowFocus: false – I’m tabbing between my editor and browser constantly. Don’t need to refetch every time.
    • retry: 1 – If the API is down, retrying 3 times just delays the error message.
    • staleTime: 5 minutes – Tasks don’t change that often. No need to hammer the API.

    The Query Hook:

    const { data: tasks, isLoading, error } = useQuery({
        queryKey: ['tasks'],
        queryFn: taskApi.getTasks,
    });

    That queryKey is the secret. React Query uses it to cache results. If I call this hook in multiple components, they all share the same cached data. Change the key, you get a new cache entry:

    // Different cache entries
    ['tasks']                    // All tasks
    ['tasks', { completed: true }]  // Completed tasks only
    ['tasks', userId]            // User-specific tasks

    The Mutation Hooks:

    const createMutation = useMutation({
        mutationFn: taskApi.createTask,
        onSuccess: () => {
            // Invalidate queries to trigger refetch
            queryClient.invalidateQueries({ queryKey: ['tasks'] });
        },
    });
    const deleteMutation = useMutation({
        mutationFn: taskApi.deleteTask,
        onSuccess: () => {
            queryClient.invalidateQueries({ queryKey: ['tasks'] });
        },
    });

    What’s happening here:

    1. Call createMutation.mutate(newTask)
    2. API request fires
    3. On success, invalidate the ['tasks'] cache
    4. React Query automatically refetches all queries using that key
    5. UI updates with new data

    No manual state updates, no Redux actions, no event emitters. Just invalidate the cache and React Query does the rest.

    Future optimization: Right now I’m invalidating the whole list on every mutation. Later, I’ll implement optimistic updates:

    // Coming in Part 3
    const createMutation = useMutation({
        mutationFn: taskApi.createTask,
        onMutate: async (newTask) => {
            // Cancel outgoing queries
            await queryClient.cancelQueries({ queryKey: ['tasks'] });
            
            // Snapshot previous value
            const previous = queryClient.getQueryData(['tasks']);
            
            // Optimistically update
            queryClient.setQueryData(['tasks'], (old) => [...old, newTask]);
            
            return { previous };
        },
        onError: (err, newTask, context) => {
            // Rollback on error
            queryClient.setQueryData(['tasks'], context.previous);
        },
    });

    This makes the UI feel instant – the task appears immediately, then syncs with the server in the background.

    Prisma: Type-Safe Database Access

    Prisma’s magic is in the generated client. Here’s my schema:

    model Task {
      id          String    @id @default(uuid())
      title       String
      description String?
      completed   Boolean   @default(false)
      dueDate     DateTime?
      createdAt   DateTime  @default(now())
      updatedAt   DateTime  @updatedAt
      
      userId String
      user   User   @relation(fields: [userId], references: [id], onDelete: Cascade)
    }

    Run npx prisma generate and it creates a fully-typed client:

    // This knows about Task, knows completed is Boolean,
    // knows description is String | null, everything
    const task = await prisma.task.create({
        data: {
            title: "New task",
            userId: "temp-user-id",
            // TypeScript will yell if I forget required fields
            // or pass wrong types
        }
    });

    The Relations:

    // Get tasks with user data in one query
    const tasks = await prisma.task.findMany({
        include: {
            user: true  // Joins the User table automatically
        }
    });
    // Type of tasks is now:
    // Array<Task & { user: User }>

    Prisma handles the JOIN, returns properly typed data, and I don’t write SQL.

    Prisma Migrations:

    # Create a migration
    npx prisma migrate dev --name add_task_priority
    # It generates SQL for me:
    # CREATE TABLE "Task" (
    #     "id" TEXT NOT NULL PRIMARY KEY,
    #     "title" TEXT NOT NULL,
    #     ...
    # )

    The migration files are in prisma/migrations/ as SQL. I can version control them, review them, modify them if needed. But 99% of the time, Prisma gets it right.

    Future posts will cover:

    • Complex Prisma queries (aggregations, groupBy)
    • Database indexing strategy
    • Handling concurrent updates
    • Soft deletes vs hard deletes

    API Client: Axios with Interceptors

    The API client has some sneaky stuff in it:

    // client.ts
    export const apiClient = axios.create({
        baseURL: API_BASE_URL,
        headers: {
            'Content-Type': 'application/json',
        },
    });
    // Request interceptor - adds auth token
    apiClient.interceptors.request.use((config) => {
        const token = localStorage.getItem('auth_token');
        if (token) {
            config.headers.Authorization = `Bearer ${token}`;
        }
        return config;
    });
    // Response interceptor - handles 401s
    apiClient.interceptors.response.use(
        (response) => response,
        (error) => {
            if (error.response?.status === 401) {
                localStorage.removeItem('auth_token');
                window.location.href = '/login';
            }
            return Promise.reject(error);
        }
    );
    

    Why interceptors?

    • Don’t have to manually add auth headers to every request
    • Global error handling – one place for 401 logic
    • Easy to add logging, retry logic, etc.

    The API functions:

    // tasks.ts
    export const taskApi = {
        getTasks: async (): Promise<Task[]> => {
            const response = await apiClient.get('/api/tasks');
            return response.data;
        },
        
        createTask: async (data: CreateTaskInput): Promise<Task> => {
            const response = await apiClient.post('/api/tasks', data);
            return response.data;
        },
    };

    Clean, typed, testable. Each function is a single responsibility. Later I can mock these for tests, or swap axios for fetch, or add retry logic.

    Coming in Part 2: Adding authentication with JWT tokens and refresh token rotation.

    Component Architecture: Keeping It Simple

    I’m using a pretty standard React component structure:

    src/
    ├── api/              # API client and functions
    │   ├── client.ts     # Axios instance + interceptors
    │   └── tasks.ts      # Task-specific API calls
    ├── components/       # Reusable UI components
    │   ├── TaskItem.tsx
    │   └── CreateTaskForm.tsx
    ├── pages/            # Full page views
    │   └── Tasks.tsx     # Main task page
    ├── types/            # TypeScript interfaces
    │   └── task.ts
    └── hooks/            # Custom hooks (empty for now)

    Why this structure?

    • api/ – Keeps all HTTP stuff in one place. Easy to mock for tests.
    • components/ – Reusable, presentational components. Pure functions of props.
    • pages/ – “Smart” components that fetch data and handle business logic.
    • types/ – Shared TypeScript interfaces. One source of truth.

    Component Design Principles:

    1. Presentational vs Container

    // TaskItem.tsx - Presentational (dumb component)
    interface TaskItemProps {
        task: Task;
        onToggle: (id: string) => void;
        onDelete: (id: string) => void;
    }
    export function TaskItem({ task, onToggle, onDelete }: TaskItemProps) {
        // Just renders UI, no data fetching
        return (
            <div>
                <input 
                    type="checkbox"
                    checked={task.completed}
                    onChange={() => onToggle(task.id)}
                />
                {task.title}
            </div>
        );
    }
    // Tasks.tsx - Container (smart component)
    export function Tasks() {
        // Handles data fetching and mutations
        const { data: tasks } = useQuery({
            queryKey: ['tasks'],
            queryFn: taskApi.getTasks,
        });
        
        const toggleMutation = useMutation({
            mutationFn: taskApi.toggleTask,
            onSuccess: () => {
                queryClient.invalidateQueries({ queryKey: ['tasks'] });
            },
        });
        
        return (
            <div>
                {tasks?.map(task => (
                    <TaskItem 
                        task={task}
                        onToggle={toggleMutation.mutate}
                        onDelete={deleteMutation.mutate}
                    />
                ))}
            </div>
        );
    }

    Why separate them?

    • Easy to test (presentational components are pure functions)
    • Easy to reuse (TaskItem doesn’t care where data comes from)
    • Easy to reason about (data logic is in pages, UI logic is in components)

    2. Form Handling – Controlled Components

    // CreateTaskForm.tsx
    const [title, setTitle] = useState('');
    const [description, setDescription] = useState('');
    const handleSubmit = (e: React.FormEvent) => {
        e.preventDefault();
        
        onSubmit({
            title: title.trim(),
            description: description.trim() || undefined,
        });
        
        // Reset form
        setTitle('');
        setDescription('');
    };

    Classic controlled components. The form state lives in React state, gets cleared on submit. Simple, predictable.

    Future improvements:

    • Form validation library (probably React Hook Form)
    • Loading states and error messages
    • Keyboard shortcuts (Cmd+Enter to submit)

    Coming in Part 3: Building a complex drag-and-drop interface and managing that state.

    TypeScript: The Type System Doing Heavy Lifting

    Every piece of data has a type. Here’s the full type system so far:

    // types/task.ts
    export interface Task {
        id: string;
        title: string;
        description: string | null;  // Nullable!
        completed: boolean;
        dueDate: string | null;      // Nullable!
        createdAt: string;
        updatedAt: string;
        userId: string;
    }
    export interface CreateTaskInput {
        title: string;
        description?: string;   // Optional on input
        dueDate?: string;       // Optional on input
    }

    Why two types?

    • Task matches the database schema exactly (includes id, timestamps, userId)
    • CreateTaskInput is what the user provides (no id yet, no timestamps)

    This prevents bugs like:

    // TypeScript error: Property 'id' is required
    const task: Task = {
        title: "New task",
        completed: false,
    };
    // This works fine
    const input: CreateTaskInput = {
        title: "New task",
        description: "Details here",
    };
    

    The Prisma types match exactly:

    // Prisma generates this
    type PrismaTask = {
        id: string;
        title: string;
        description: string | null;
        // ... matches our Task interface
    }
    

    End-to-end type safety: database → API → frontend. Change the schema, and TypeScript will yell at you everywhere that needs updating.

    Coming in future parts: Discriminated unions for different task types, generics for reusable API functions, and advanced TypeScript patterns.

    • Hardcoded user ID: 'temp-user-id' living her best life in production-ish code
    • No error boundaries: If something breaks, the whole app goes down like a house of cards
    • No loading skeletons: Just raw “Loading…” text like it’s 2010
    • No form validation: Beyond required fields, you can put whatever cursed data you want in there
    • Zero tests: I’m a senior engineer who knows better, which makes this worse

    I’ll fix it. Eventually. Maybe. (I won’t.)

    The Technical Debt I’m Acknowledging (For Now)

    Let me be real about what’s not production-ready:

    1. The Hardcoded User ID

    const userId = 'temp-user-id';  // Living her best life in every route

    Every API call uses this. No authentication, no user sessions, nothing. Just vibes.

    Why this is actually okay for now: I’m building the task management features first. Once they work, I’ll add auth. Building auth first would mean testing with fake data anyway. This way, I’m testing with real CRUD operations.

    Coming in Part 2: JWT authentication, refresh tokens, and proper session management.

    2. No Error Boundaries

    If a component throws, the whole app crashes. React 18 has error boundaries, I just… didn’t implement them yet.

    // What I should have
    <ErrorBoundary fallback={<ErrorMessage />}>
        <Tasks />
    </ErrorBoundary>
    // What I actually have
    <Tasks /> // YOLO

    3. Loading States Are Basic

    {isLoading && <p>Loading...</p>}

    No skeletons, no smooth transitions, just plain text. It works but it’s not pretty.

    Future improvement: Loading skeletons with Suspense:

    <Suspense fallback={<TaskListSkeleton />}>
        <TaskList />
    </Suspense>

    4. No Tests

    Zero. Zip. Nada. I know, I know. I’m a senior engineer. I should know better.

    My excuse: I’m still figuring out what I’m building. Once the features stabilize, I’ll add:

    • Unit tests for API functions
    • Component tests with React Testing Library
    • E2E tests with Playwright

    Reality check: I’ll probably add tests when something breaks in production and I get scared.

    5. Form Validation Is Minimal

    <input 
        type="text"
        required  // That's the whole validation
    />

    No character limits, no XSS protection, no sanitization. Just raw faith in humanity.

    Coming soon: React Hook Form + Zod for proper validation:

    const schema = z.object({
        title: z.string().min(1).max(200),
        description: z.string().max(1000).optional(),
        dueDate: z.string().datetime().optional(),
    });

    What’s Next: The Roadmap

    This is Part 1 of an ongoing series. Here’s what’s coming:

    Part 2: Authentication & User Management (Next Post)

    • JWT authentication flow
    • Refresh token rotation
    • Protected routes
    • User registration and login
    • Session persistence
    • “Remember me” functionality

    Technical challenges I’ll dive into:

    • Secure token storage (localStorage vs httpOnly cookies)
    • Handling token expiration gracefully
    • Race conditions with multiple API calls
    • CSRF protection

    Part 3: The Daily Planning View (The Hard Part)

    • Drag and drop tasks into today’s schedule
    • Time blocking interface
    • Visual timeline of the day
    • Calculate total planned time vs available time
    • Conflict detection (overlapping time blocks)

    Technical deep dives:

    • React DnD vs react-beautiful-dnd vs dnd-kit (spoiler: I’ll pick dnd-kit)
    • Handling drag state in React Query
    • Optimistic updates for drag operations
    • Time zone handling (moment.js is dead, long live date-fns)

    Part 4: The Stuff That Makes It Actually Useful

    • Calendar integration (Google Calendar, Outlook)
    • Recurring tasks
    • Task priorities and categories
    • Search and filtering
    • Keyboard shortcuts
    • Dark mode (legally required for developer tools)

    Part 5: Performance & Polish

    • Code splitting and lazy loading
    • Image optimization (when I add avatars)
    • PWA setup (offline support!)
    • Bundle size optimization
    • Lighthouse score improvements
    • Real-time updates with WebSockets or polling

    Part 6: Mobile & Desktop Apps

    • React Native for mobile
    • Electron for desktop
    • Sharing code between web/mobile/desktop
    • Platform-specific features

    Part 7: The Production Stuff

    • Error tracking (Sentry)
    • Analytics (PostHog or Plausible)
    • A/B testing framework
    • Monitoring and alerts
    • Database backups and disaster recovery
    • Deployment pipeline (GitHub Actions)

    Bonus Content (TBD):

    • Building a Chrome extension
    • API rate limiting
    • Billing and subscriptions (Stripe)
    • Team collaboration features
    • Data export/import
    • GDPR compliance

    Each post will be a deep technical dive into that feature, including:

    • Architecture decisions and tradeoffs
    • Code walkthroughs with explanations
    • Things that went wrong and how I fixed them
    • Performance considerations
    • Testing strategies

    Why Document This Journey?

    Selfishly: Writing forces me to understand my decisions. If I can’t explain why I chose React Query over Redux, I don’t understand it well enough.

    Hopefully useful to you: There are a million tutorials on building todo apps. There aren’t many on building a real app with real features, real technical debt, and real architectural decisions.

    I’m documenting:

    • The thought process behind technical choices
    • The things that didn’t work (Docker, I’m looking at you)
    • The pragmatic tradeoffs (Supabase instead of local DB)
    • The “I’ll fix it later” decisions that we all make

    This is how real apps get built. They are built iteratively and imperfectly. There is a lot of Googling involved. Some questionable shortcuts are taken, which you promise you’ll fix before launch (narrator: she did not fix them before launch).

    Lessons From Part 1 (That I’ll Probably Ignore Next Time)

    1. Pragmatism > Perfection

    Docker wasn’t working? Cool, Supabase exists. Ship it. I could’ve spent a week getting the perfect local development environment. Instead, I spent 30 seconds getting a working database.

    The meta-lesson: Done is better than perfect, but “done” doesn’t mean “shipped to production.” It means “working well enough to learn from.”

    2. Boring Technology Is Good Technology

    Express, React, PostgreSQL. Nothing fancy, nothing new. It all just works because thousands of people have already hit the edge cases.

    Future me will thank past me for not choosing the hot new framework that’ll be deprecated in 6 months.

    3. TypeScript Everywhere or TypeScript Nowhere

    Half-typed code is worse than no types. You get a false sense of security. Go full TypeScript or don’t bother.

    // This is a lie - response.data could be anything
    const task = await apiClient.get('/api/tasks') as Task;
    // This is honest - we're validating at runtime
    const task = TaskSchema.parse(await apiClient.get('/api/tasks'));

    Coming in a future post: Runtime validation with Zod to make this bulletproof.

    4. React Query Is Magic (No Really)

    If you’re doing data fetching in React and not using React Query (or SWR or RTK Query), you’re missing out. The amount of boilerplate it eliminates is staggering.

    Before React Query:

    • Manual loading states
    • Manual error handling
    • Manual cache invalidation
    • Manual optimistic updates
    • Manual retry logic
    • Crying

    After React Query:

    const { data, isLoading, error } = useQuery({
        queryKey: ['tasks'],
        queryFn: getTasks,
    });

    5. Build For Real Use Cases

    I’m building this because I need it. Every feature I add, I’m thinking: “Would I actually use this? Does this solve my problem?”

    That focus keeps me from:

    • Over-engineering (do I really need microservices for a task app?)
    • Under-engineering (can’t skip auth forever)
    • Bikeshedding (who cares what the button color is if the app doesn’t work)

    6. Document While Building, Not After

    I’m writing this blog post while the experience is fresh. The Docker pain is still real. The Tailwind confusion is still visceral. The “oh that’s why” moments are still memorable.

    If I waited until the app was done: “I set up the database, it was fine.”

    Writing it now: “I spent too long fighting Docker. I questioned my life choices. Then I set up Supabase and felt both defeated and relieved.”

    The Journey Continues

    This is just the foundation. The app works, but it’s not useful yet. It’s a todo list. The world has enough todo lists.

    What makes Sunsama special is the daily planning flow. It’s the ritual of looking at your tasks. You pick what you’ll do today. You assign time blocks. You see your day laid out visually. That’s where the magic happens.

    Building that is going to be hard. Drag and drop in React is notoriously finicky. Time calculations get weird. State management gets complex. I’m excited.

    What’s Actually Working Right Now

    Let me be clear about where we are:

    ✅ Fully functional:

    • Create tasks with title, description, due date
    • Mark tasks as complete
    • Delete tasks
    • Real-time UI updates
    • Type-safe end-to-end
    • Deployed and accessible (well, running locally)

    ⚠️ Works but needs improvement:

    • No authentication (using temp-user-id)
    • Basic error handling
    • Simple loading states
    • Minimal validation

    ❌ Not built yet:

    • Daily planning view
    • Time blocking
    • Calendar integration
    • Recurring tasks
    • Mobile app
    • Literally everything else

    The app at this stage: A solid foundation with good architecture decisions. Not production-ready, but ready for the next features.

    Next Post Preview

    Part 2: Authentication & User Management is coming next week. I’ll cover:

    • JWT authentication implementation
    • Secure token storage (and why localStorage is controversial)
    • Session management and refresh tokens
    • Protected routes in React Router
    • Migration from temp-user-id to real users

    Technical question I’ll answer: Should you store JWTs in localStorage, sessionStorage, or httpOnly cookies? (Spoiler: everyone has opinions, all of them are confident, and they’re all slightly wrong)

    Follow along:

    • GitHub: [Coming soon – I’ll make the repo public]
    • Twitter: [Your handle if you want]
    • RSS: [If you’re into that]

    Final Thoughts

    I started this project to solve my own task management problems. Three hours later, I have a working CRUD app. I also have a lot of technical debt. I now deeply appreciate how much work goes into “simple” apps.

    The codebase is clean enough to build on. The architecture is solid enough to scale. The features are minimal enough that I’m not overwhelmed. This is the sweet spot.

    Next weekend: Authentication. Then the daily planning view. Then calendar integration. Then… we’ll see.

    The journey from “working app” to “app I actually use every day” is going to be interesting. I’ll document every step. I’ll capture the wins and the failures. I’ll note the moments of “why did I think this was a good idea.” I’ll also capture the surprises of “oh hey, that actually worked.”

    Come along for the ride. It’s going to be educational, occasionally painful, and hopefully entertaining.


    Thanks for reading Part 1! Questions? Suggestions? Think my architecture decisions are terrible? Hit me up – I’m genuinely curious what other engineers would do differently.

    Next post drops next week. Subscribe/follow/whatever kids do these days to get notified.

    P.S. – Yes, I know there are 10,000 task management apps. No, that won’t stop me from building another one. This is the way.

    P.P.S. – The app still doesn’t help me actually complete tasks. But now I can procrastinate in a type-safe, architecturally sound way. Progress?

  • Analyzing My Academic Journey

    Analyzing My Academic Journey

    Over the past year, I’ve taken various courses as part of my journey toward obtaining my Master’s degree, and these courses have greatly influenced my final project, Task Alchemist. In this blog post, I will discuss the significant roles played by each class in shaping the project, the challenges I encountered, and my thoughts on the curriculum’s strengths and areas for improvement.

    Research Approaches in Computer Science

    Contribution: This course laid the groundwork for understanding various research methodologies and their application in computer science. The skills I gained here were necessary for developing and structuring the research approach for Task Alchemist later on.

    Challenges: The class was intense because I had to read and analyze many research papers about Ad Tech ethics while working on my paper. It was initially challenging to apply various research methods to a practical project.

    Curriculum Feedback: The course was thorough and enlightening, providing the foundation for research-based development practices.

    Advanced Software Engineering

    Contribution: This class provided advanced knowledge in software design and development practices, which was essential for Task Alchemist’s architecture. It focused on best practices in coding, testing, and project management.

    Challenges: Balancing theoretical knowledge with practical application was challenging.

    Curriculum Feedback: The material was high-quality, but including more real-world scenarios would be beneficial.

    Data Science

    Contribution: The data science course was vital in learning how to handle and analyze large datasets, which is crucial for the AI components in Task Alchemist. Techniques learned here were directly applied to optimize task scheduling.

    Challenges: Balancing theoretical knowledge with practical application was challenging. Creating detailed milestones and working extensively on design documents required significant effort.

    Curriculum Feedback: The instruction was engaging, and even with my professional experience, I learned quite a bit about software design and best practices. My instructor was fair and personable and took the time to make sure we understood the material. However, I wish more practice was involved in sequencing and UML diagramming.

    Data Visualization and Extended Reality

    Contribution: This class helped me understand the significance of effectively presenting data, a crucial aspect of Task Alchemist’s user interface. The skills acquired here ensured that the data visualizations were functional and visually appealing.

    Challenges: Integrating extended reality concepts into a traditional data visualization framework took a lot of work.

    Curriculum Feedback: More examples of extended reality applications in mainstream projects would be helpful.

    Advanced Artificial Intelligence

    Contribution: This course was essential in developing the AI algorithms that power Task Alchemist. It covered advanced topics in AI that are directly applied to enhancing task prioritization and scheduling.

    Challenges: The depth and breadth of AI topics were overwhelming at times.

    Curriculum Feedback: The material quality was excellent, but more interactive sessions could help solidify complex concepts.

    Machine Learning

    Contribution: Machine learning techniques were essential for training the models used in Task Alchemist. This course provided the theoretical and practical knowledge to implement these techniques effectively.

    Challenges: Mastering and implementing advanced machine learning algorithms required significant effort. Finding suitable datasets for specific tasks was also challenging at times.

    Curriculum Feedback: The course met expectations, and I particularly enjoyed the course format.

    Human-Computer Interaction

    Contribution: This course provided insights into designing user-centric applications. The principles learned were applied to make Task Alchemist intuitive and user-friendly.

    Challenges: Balancing user needs with technical constraints was challenging.

    Curriculum Feedback: The course provided valuable insights, and combining class discussions with practical applications enhanced the learning experience.

    Human-Computer Interaction App Development

    Contribution: In this class, I gained practical experience in developing HCI applications. It built on the foundational knowledge from the Human-Computer Interaction class and directly contributed to building the front end of Task Alchemist. I developed an application that visualizes data and creates an interactive experience using gamification principles to incentivize advertisement engagement.

    Challenges: Ensuring seamless integration between the user interface and backend functionality was challenging. Balancing aesthetic design with usability and technical constraints required careful consideration and iterative testing.

    Curriculum Feedback: The course content was excellent and significantly made my final project more engaging.

    Software Project – Research, Planning, and Design

    Contribution: This was Task Alchemist’s inception phase. It provided a structured approach to project planning and design.

    Challenges: Defining the project scope and objectives was challenging.

    Curriculum Feedback: The course was well-structured, but more guidance on project scoping would be helpful.

    Software Project – Development I & II

    Contribution: I dedicated my time to developing Task Alchemist during these courses. Regular check-ins with my advisor were crucial in receiving valuable guidance and feedback, which helped refine the project’s scope and implementation. This collaboration ensured that the project aligned with my goals and academic standards.

    Challenges: Effectively managing time and resources presented a significant challenge. It required careful planning and prioritization to balance multiple development tasks and incorporate ongoing feedback.

    Curriculum Feedback: The courses were practical and well-structured; however, incorporating regular peer reviews could enhance the learning experience by providing diverse perspectives and additional feedback, ensuring continuous progress, and fostering a collaborative learning environment.

    Software Project – Deployment and Professional Presentation

    Contribution: This final course focused on refining professional presentation skills and deployment strategies for Task Alchemist. It established a solid framework to ensure the project was polished and prepared for public presentation.

    Challenges: The main challenges were ensuring the project was deployment-ready and presenting it professionally. It was important to work with my advisor to see this through, complete my last two milestones, finalize my thesis, and prepare to present to the capstone committee.

    Curriculum Feedback: The course was incredibly valuable. Working closely with my advisor was essential, but having more opportunities for peer feedback on presentations would further enhance readiness and confidence.

    Overall Reflections

    Timely and Meaningful Feedback: Throughout the program, I consistently received timely and insightful feedback that played a crucial role in enhancing my learning experience and advancing the development of my projects.

    Quality of Material and Instruction: The courses were carefully structured, offering in-depth insights and a thorough understanding of each subject.

    Suggestions for Improvement

    1. More Real-World Examples: Integrating more real-world case studies and examples across all courses could enhance practical understanding.
    2. Interactive Sessions: Increasing the number of interactive and hands-on sessions, especially in complex subjects like AI and machine learning, would be beneficial.
    3. Collaborative Projects: Encouraging more teamwork and collaborative projects could help develop essential soft skills.

    Conclusion

    The educational program provided an in-depth and strong foundation in computer science and software engineering, encompassing various subjects and offering practical exposure to multiple technologies. The theoretical understanding and hands-on proficiency I have acquired will significantly enhance my performance in upcoming endeavors and professional paths.

  • Task Alchemist Development Update: Another Month of Progress

    Task Alchemist Development Update: Another Month of Progress

    The past month has been exciting as I’ve focused on transforming Task Alchemist into a more sophisticated and intuitive task management tool. My goal has always been to optimize task scheduling by blending AI and cognitive psychology insights. Here’s a summary of the latest progress.

    Streamlined Calendar Integration

    One of my key development efforts was improving the calendar integration to seamlessly import external calendars. Specifically, I implemented the feature to import Google Calendar data directly into FullCalendar. Each imported event is tagged with the property source: 'google', ensuring that all external events are easily distinguishable.

    Firebase Tasks Scheduled Around Imported Events

    A significant challenge was enabling Firebase tasks to be scheduled around imported Google Calendar events. This feature required a delicate balance between optimizing productivity and respecting external commitments. I tackled this by calculating priority weighting with a Cloud Function and ensuring that task data is dynamically scheduled in a way that adapts to each user’s existing commitments.

    Reinforcement Learning and Q-Learning

    To achieve my ambitious AI scheduling goals, I’ve been feeding real task data into my reinforcement learning setup, leveraging TaskEnv as the environment. I aim to enable the system to learn from real-world task patterns, ensuring that each schedule recommendation aligns with a user’s natural workflow.

    Preliminary Surveys and Feedback

    The best features are shaped by real-world usage. I conducted preliminary surveys with testers, focusing on features like task creation, list management, calendar scheduling, and Google Calendar imports. The feedback collected has been invaluable for refining preliminary AI scheduling features, helping me identify opportunities for improvement before launching publicly.

    Looking Ahead

    As I move into the next phase of development, I will refine the reinforcement learning algorithms and enhance the overall AI-driven task prioritization framework. I’m also incorporating cognitive psychology insights into the scheduling algorithms to ensure a balance of efficiency and user satisfaction.

    Stay tuned for more updates, and thank you for following my journey with Task Alchemist!

  • Building Blocks: Getting to know me, and an Introduction to my Masters Capstone Project

    Building Blocks: Getting to know me, and an Introduction to my Masters Capstone Project

    I’m Morgan, a software engineer pursuing a master’s degree in computer science. I’ve been in the tech industry for many years, exploring various areas of software engineering, from backend development to AI. I’m particularly interested in creating impactful user experiences through thoughtful design and robust infrastructure.

    “The Coded Path” is my portfolio blog, documenting my journey through my master’s capstone project and the broader world of technology. I’ll be sharing insights into the challenges I encounter, the solutions I develop, and the lessons I learn along the way. I hope this blog will serve as a resource and inspiration for others, especially women in tech.

    My master’s capstone project, “Task Alchemist,” aims to redefine productivity through a task management application that leverages AI and NLP. It’s designed to personalize the productivity experience and simplify task prioritization and organization for personal planning.

    This month, “Task Alchemist” hit two major milestones. Setting up Firebase was like nailing the first piece of a giant jigsaw puzzle, and implementing user authentication added a layer of professionalism and trust to the app.

    However, I encountered unexpected challenges while setting up the CI/CD integration between Firebase and GitHub. After reconfiguring the CI/CD pipeline and addressing the issues with the configuration files, I successfully deployed, which was a significant learning moment.

    I’m planning a thorough review of my milestones to ensure smooth progress. The journey so far has taught me the importance of detailed planning and the flexibility to adapt. I’m more committed than ever and eager to apply the insights gained, overcome upcoming challenges, and keep learning and adapting.

    Reflecting on my time on “Task Alchemist” so far, I feel a deep sense of accomplishment and learning. Balancing the demands of this project with the rest of the demanding needs of life, as I am a full-time engineer at an AdTech company and a mom, has been a significant challenge, but it’s also been incredibly rewarding. I’ve learned to prioritize tasks (ironically), break down complex problems into manageable pieces, and stay flexible in my planning. These skills have been instrumental in keeping the project on track and making meaningful progress.

    The coursework from my previous classes has been invaluable in shaping “Task Alchemist.” Concepts from my AI and machine learning courses and topics revisited during my Advanced Software Engineering classes have been directly applicable, especially in terms of project management and design principles. My studies in artificial intelligence and natural language processing have also been foundational to developing the app’s unique features. Seeing theories and concepts from class come to life in my project is fascinating, giving me a hands-on opportunity to apply what I’ve learned.

    My interactions with my advisor this month have been incredibly productive. We’ve discussed the project’s direction, and their insights have helped refine my approach, especially in planning and preparing my design documents and project proposal. Their guidance has encouraged me to think critically about the project’s future phases. These conversations have been pivotal in ensuring that “Task Alchemist” remains innovative and on course to meet its objectives.

    I feel ready and energized as I prepare for the next development phase, albeit mindful of the challenges ahead. The experiences of this month have highlighted the importance of continuous learning and adaptation. I plan to apply these lessons by proactively seeking feedback, refining my project management strategies, and staying open to new approaches and solutions. The journey of “Task Alchemist” is far from over. Still, I’m more confident than ever in navigating it and bringing this vision to life.

    If you are interested in following the development, the app is located here: https://task-alchemist.web.app/