*This is Part 2 of the series on building a Production ready application with Copilot and Mermaid. In Part 1, we built the basic functionality with drag-and-drop. In this second session the intent was to setup identity providers/auth and a persistent storage system.

This… is not the way things worked out.

Picking up from last time

Opening the project folder “restored” the chat history fro the previous session, which made it easy to get started.

At this time, I am ready for us to start working on part 2, which will be all about Adding persistence and identity management layers. Again, please keep a record of what we do in this new session, so that we can write a blog post at the end specific to Part 2

I then provided Copilot with a MermaidJs diagram showing the proposed flow for user identity actions

flowchart TD
subgraph Register["Create account page"]
EnterDetails["Enter Registration Details"]
EmailExistsFail["Show Already exists message"]
VerifyEmail["VerifyEmail"]
end
subgraph Login["Login page"]
EnterCredentials["Enter username/pw"]
ForgotPWLink["Click forgot password"]
RegisterLink["Click Create new account"]
end
subgraph ForgotPassword["Forgot PW page"]
EnterEmail["Enter Email"]
EmailReset["Click PW Reset Link"]
SetNewPwd["Set New Password"]
ResetSuccess["Password Reset Success"]
ResetFail["Password Reset Failed"]
end
subgraph SNewUser["Create User"]
CheckEmail["CheckEmail"]
EmailExists["EmailExists"]
CreateAccount["Save account details"]
EmailVerification["Send verification email"]
end
subgraph Supabase["Supabase"]
SNewUser
SAuth["User Authentication"]
SAuthStatus("Auth status")
Storage["Storage"]
SAuthChange["Update password"]
end
subgraph PasswordReset["Password Reset page"]
UpdatePwd["Update Password (Logged In)"]
EnterOldNewPwd["Enter Old/New Password"]
UpdateSuccess["Password Update Success"]
UpdateFail["Password Update Failed"]
end
subgraph LoggedIn["Logged In"]
Dashboard["View board list"]
UserProfile["UserProfile"]
PasswordReset
PasswordResetLink["Click Change password"]
ShowError["ShowError"]
end
User(["User"]) --> Home["Home page"] & VerifyEmail
Home --> Login
RegisterLink -- Register --> EnterDetails
ForgotPWLink -- Reset PW --> ForgotPassword
EnterCredentials --> SAuth
SAuth --> SAuthStatus & Storage
SAuthStatus -- Success --> LoggedIn
SAuthStatus -- Fail --> Login
EnterDetails -.-> SNewUser
CheckEmail --> EmailExists
EmailExists -- No --> CreateAccount
EmailExists -- Yes --> EmailExistsFail
EmailExistsFail --> EnterDetails & ForgotPassword
CreateAccount --> EmailVerification & Storage
UserProfile --> PasswordResetLink
ForgotPassword --> EnterEmail
EnterEmail --> EmailReset
EmailReset --> SetNewPwd
SetNewPwd -- Success --> ResetSuccess
SetNewPwd -- Fail --> ResetFail
UpdatePwd --> EnterOldNewPwd
EnterOldNewPwd --> SAuthChange
SAuthChange -- Success --> UserProfile
SAuthChange -- Fail --> ShowError
UpdateFail --> UpdatePwd
ResetSuccess --> Login
ResetFail --> ForgotPassword
EmailVerification -.-> User
SAuthChange --> Storage
LoginPage["LoginPage"]
EmailExists@{ shape: diam}
SAuth@{ shape: subproc}
Storage@{ shape: cyl}
SAuthChange@{ shape: subproc}

Constant summary and feedback

Copilot is pretty good about tracking things that you have asked, and at the end of each step provides you with options for what to do next.

Understood! We’ll use Supabase for both authentication and persistence, following the user identity flow you provided. I’ll keep a detailed log of all steps, code, and decisions for your Part 2 blog post.

…
Let’s start by creating the Part 2 folder and setting up Supabase in the project.

At this point Copliot generated all of the code for the login/logout functionality. Then it asked if I wanted it to work on the registration flow, or something else.

Once all of this work was completed I noticed that all of the new code was being added to the ./Part2 folder, as opposed to updating the existing source. I had to ask for this to be fixed.

First I want to take all of the code we are creating for part 2, and only update the existing files in the ./src/ folder as well to ensure that we continue to have a completely operational website.

[…]
Can we fix all of the files so that everything we work on today is updating the base set of files in the project root and in the ./src/ folder. We will only add these files to the Part 2 folder after we have completed all of the necessary work


Copilot made all of the changes, moving/deleting files as needed, fixing the imports and all of the Svelte warnings. The AI iterated through each file fixing errors, returning to files already modified if there were additional errors.

We ran all of the new tests created… and they failed.

The first challenge: setting up Supabase

I went through a lot of troubleshooting and back and forth conversations with Copilot to try and figure out what was happening, with no real success. I even tried to download the local dev container for Supabase with no luck. Part of this failure is that when trying to init Supabase locally the system needs to connect to an external resource, which was failing.

I was thinking that the issue may have been caused by the work network. When I got home, I let Copilot know that I had switched networks, and we should try all of the supabase tests again. They still failed.

In the end, the issue was caused by a combination of system admin managed firewall rules on my work computer, as well as the network/proxy configuration.

I shouldn’t have tried to work on this project from that system in the first place, so I pushed the repo to GitHub so I could continue from my personal computer.

Creating a Storage Abstraction Layer

Rather than being blocked by external dependencies, I asked Copilot to create an abstraction layer that would allow us to work with services that would use either an in-memory storage or a real Supabase back-end, based on a flag in the environment variables.

What was generated was an interface and a “mock” implementation for each service that mimicked what we had already created for Supabase. This created services that were tightly coupled to specific implementations. We had separate mock services and Supabase services, leading to code duplication and maintenance overhead.

This was not quite what I wanted. I wanted an interface for the storage system that could be swapped out and used by the services.

important note: if you are not specific in what you want, AI will generate the closest thing to what you actually asked for, which may not be what you want πŸ˜‰

Can we create an interface for the database activities for all of our services that can be used as a wrapper for either Supabase implementation, or our in memory database. That way we can switch which one we are using in the tests?

The Solution: Storage Interface Pattern

// StorageInterface.ts
export interface StorageInterface {
  findAll<T>(table: string): Promise<T[]>;
  findById<T>(table: string, id: number): Promise<T | null>;
  findWhere<T>(table: string, field: string, value: any): Promise<T[]>;
  findWithRelations<T>(table: string, id: number, relations: string[]): Promise<T | null>;
  insert<T>(table: string, data: Omit<T, 'id'>): Promise<T>;
  update<T>(table: string, id: number, updates: Partial<T>): Promise<void>;
  delete(table: string, id: number): Promise<void>;
}

This allowed us to create two implementations:

  • InMemoryStorage – For development and testing
  • SupabaseStorage – For production

Centralized Decision Making

At this point each of the services was updated to select the storage solution based on a flag in the environment variables. But I wanted to have this logic in a single location.

I would like there to be one place where I can set whethere or not to use in memory or supabase. Can we use server.jooks.ts for this?

Notice the typo… which led to this response:

Yes, you can centralize the data source selection in a single file, such as server.jooks.ts (or more conventionally, server.hooks.ts or serverConfig.ts). This file can export a flag or function to determine whether to use in-memory mocks or Supabase, and all your service imports can reference it.

How to implement:
…

Would you like me to scaffold this setup and refactor your service imports to use a central toggle?

To make sure that it was clear how I wanted the solution to be handled, I restated my request with a more explicit prompt:

I want to use the SvelteKit server hooks process which creates things needed in a server variable called locals that will look at the environment variables to see if it is in test mode or now, and if it is in test mode then we add the in memory services, or if not in test mode it loads Supabase.

This helped Copilot understand what I was actually looking for, and it proceeded to scaffold all the necessary code and refactor the endpoints (it started with one, then asked if I wanted to continue with the others)

Second challenge: Switching between computers

Prompt data is not stored in a way that you can transfer the conversation history between systems.
Since I had already started the work on one computer there was a lot of data on what we were trying to achieve and what we had done so far in the local prompts. Since this information will not transfer between systems, when I cloned the repo to my personal machine all of the prompt data would be lost.

To assist with this process, I asked Copilot to generate a summary of everything done so far that would allow me to get things started on a different computer. It generated a pretty good, concise, summary that also included outstanding issues and next steps.

Iteration limits

After 25 prompts VSCode will ask if you want Copilot to continue iterating. There is a setting that controls this, and I increased it to 100.

|Copilot has been working on this problem for a while. It can continue to iterate, or you can send a new message to refine your prompt. Configure max requests.

The Interface Problem

As mentioned earlier, Copilot had originally created an implementation of an interface for each service, instead of abstracting a tthe storage level.

Working with AI made it trivial to remove these unnecessary interfaces. The AI could:

  1. Search for all usages of the interface types
  2. Update imports across the entire codebase
  3. Change type definitions in one sweep
  4. Remove the interface files themselves

What would have been a tedious, error-prone manual process became a simple conversation: “Let’s remove the interface files since we only have one implementation.”

Note: all of the interfaces that Copilot had created so far were named starting with an I. This is a common pattern in .NET, but I had not seen this much in Typescript projects.

The Power of AI-Assisted Refactoring

After this step there was still a small problem. Copilot created new files for each service, and prefaced each new implementation with Unified. I asked it to remove all of the old service files, and remove “Unified” from the new files.

Copilot:

  1. Renamed files from UnifiedBoardService.ts to boardService.ts
  2. Updated class names from UnifiedBoardService to BoardService
  3. Found and updated all imports across dozens of files
  4. Updated type definitions in app.d.ts
  5. Updated API route handlers
  6. Updated test files

This kind of comprehensive rename would take hours manually and would likely miss some references. With AI assistance, it was completed in minutes with 100% accuracy.

File Organization Made Easy

One of the biggest advantages of working with AI was how easy it made large-scale refactoring. For example, we reorganized our models from scattered locations into a clean domain-organized structure:

Before:

src/lib/models.ts (mixed models)
src/lib/client/models/models.ts (duplicate models)

After:

src/lib/models/
β”œβ”€β”€ index.ts           # Central exports
β”œβ”€β”€ User.ts           # User domain
β”œβ”€β”€ Board.ts          # Board domain
β”œβ”€β”€ Card.ts           # Card domain
β”œβ”€β”€ List.ts           # List domain
β”œβ”€β”€ Label.ts          # Label domain
β”œβ”€β”€ Comment.ts        # Comment domain
└── models.spec.ts    # Tests

The AI handled:

  • Moving model definitions to appropriate files
  • Updating all import statements across the code-base
  • Ensuring no references were broken
  • Maintaining type safety throughout

The Storage Persistence Challenge

Initially, we had an issue where the in-memory storage was being recreated on each request, causing data to disappear between page loads. This made testing the UI frustrating.

The Singleton Solution

We implemented a singleton pattern for the storage instances:

let persistentInMemoryStorage: InMemoryStorage | null = null;
let persistentSupabaseStorage: SupabaseStorage | null = null;

This ensured that during development:

  • Data persisted across page navigation
  • Manual testing became much more effective
  • The development experience closely mimicked production

Real-Time Browser Testing

The Feedback Loop

One of the most effective patterns we established was:

  1. Make changes to the code
  2. Test in the browser immediately
  3. Report any issues back to the AI
  4. Get fixes implemented quickly

Example: Date Serialization Bug

When testing drag-and-drop with newly created cards, we encountered:

Uncaught TypeError: card.createdAt.toISOString is not a function

The AI immediately understood the issue:

  • New cards from API had date strings, not Date objects
  • Drag-and-drop expected Date objects for serialization
  • We needed consistent date handling

The solution was a utility function that handled both cases.

Avoiding Lost Work

One lesson learned was the importance of using the “Keep” button after completing each major section. This prevented losing progress when experimenting with new ideas or when conversations got reset.

Best Practice: After each successful feature implementation or refactoring, click “Keep” to preserve the current state.

Key Benefits of AI-Assisted Development

1. Fearless Refactoring

With AI assistance, large-scale architectural changes became low-risk operations. The AI could comprehensively update all references, making refactoring safe and fast.

2. Comprehensive Search and Replace

Finding all usages of a particular pattern across the codebase was instantaneous. This made it easy to:

  • Update import statements
  • Change API signatures
  • Rename classes and interfaces
  • Move files between directories

3. Real-Time Problem Solving

Browser testing combined with immediate AI assistance created a rapid feedback loop for fixing issues.

4. Pattern Recognition

The AI could quickly identify common patterns (like date serialization issues) and provide proven solutions.

Lessons Learned

  1. AI excels at comprehensive refactoring that would be error-prone manually
  2. Real-time browser testing provides invaluable feedback
  3. Use “Keep” frequently to avoid losing progress
  4. Chat history is session based You cannot access chat history from different computers, so plan accordingly
  5. Preserve context between sessions with detailed prompt files

What’s Next?

In Part 3, we’ll focus on:

  • Identity/Authentication
  • Advanced UI interactions and animations
  • Real-time collaboration features
  • Performance optimization

The architecture we’ve built provides a solid foundation for these advanced features, demonstrating how thoughtful early decisions pay dividends as applications grow.


The complete code for this project is available on GitHub, and you can see the evolution through the commit history.

Leave a Reply