Best Practices for Building a SaaS with Windsurf and Makerkit

Windsurf is a new AI-powered editor taking the developer experience to the next level. With the new optimized rules for Makerkit, building a SaaS just got a lot easier!

With the release of the Windsurf rules for Makerkit, we're excited to announce that we're now offering enhanced Windsurf support for Makerkit projects. 🚀

In this post, I want to show you the best practices to build a SaaS with Windsurf and Makerkit. Windsurf is a powerful new AI Editor that can help us build faster thanks to an extremely intelligent AI and unparalleled UX.

Windsurf Workspace

Many of Makerkit's customers have started using Windsurf to build their SaaS. As such, this pushed me to optimize a set of rules to improve Windsurf's ability to work within a Makerkit project, and to allow me to understand where Windsurf is lacking so that we can improve it.

While Windsurf does an extremely good job at generating and understanding code, remember that the driver is us. We need to be able to ask the right questions, and most importantly, understand the answers and when it's misleading into wrong solutions.

Leveraging Windsurf's capabilities and understanding its shortcomings will help us build a better SaaS faster.

This is a follow-up post to the Cursor's one, where I basically followed the same steps to build a SaaS with Cursor.

Disclaimer: I will be the average Makerkit user

In some of my interactions with Windsurf, I will use as little knowledge as possible with regard to the code.

I am putting myself in the role of a new customer, not an expert in the codebase. This is to ensure that I can interact with Windsurf just like a regular customer would.

Some of my prompts may be naive or even incorrect, this is also intentional. I don't want to make any assumptions about the codebase or the developer's knowledge, but I want to provide the best possible experience for the user.

Additionally, I will make as few manual changes as possible to the codebase.

What You'll Learn 🎯

  • How to effectively use Windsurf with Makerkit's architecture
  • Creating a database schema with Windsurf and creating the relative tests
  • Improve your questions using a multi-step approach
  • Building a page with Windsurf
  • Building forms with Windsurf
  • Reacting to forms with Server Actions
  • Tips for maintaining code quality with AI assistance

Windsurf Rules for Makerkit

Makerkit provides a set of rules that you can use to optimize Windsurf's ability to work within a Makerkit project. These are store in the .windsurfrules file at the root of your project, and help you to understand:

  • Project structure and conventions
  • Common patterns and components
  • Import paths and aliases
  • Type definitions and schemas

These rules are in continuous development, and will be updated as we add new features to Makerkit.

💡 Pro Tip: Keep your .windsurfrules file updated with your SaaS business logic. This helps Windsurf suggest the best possible solutions.

Unfortunately, Windrsurf's rules are limited to 6000 characters per workspace (plus a further 6000 characters as global context). This limits the ability to provide as much context as in Cursor, but we will make it work.

👉 Learn more about the Windrsurf rules for Makerkit.

Practical Examples of using Makerkit with an LLM

In the following sections, I will show you how to use Makerkit and Windsurf to build common tasks such as:

  1. Database: building, refining and testing a Postgres/Supabase schema with Windsurf based on Makerkit's schema
  2. Building a Page: building a page with Windsurf with the same layout as Makerkit's other pages
  3. Data Fetching: fetching data from Supabase using Windsurf
  4. Building Forms with Cursor: building forms with Windsurf and using Zod to validate the data
  5. Reacting to Forms with Server Actions: using Windsurf to build forms and then using Server Actions to handle the data

Q&A about the codebase

Windsurf can be a great assistant to help you understand the codebase better and faster. As familiarizing with a codebase can be challenging, let's see how we can use Windsurf to help us understand the codebase. Let me break down the project organization and explain where to add new features.

Q1. How is the project organized and where should I add my new features?

I asked Windsurf to explain the project organization and where to add new features. Here's the response:

Let me help you understand the project organization. I'll examine the project structure to give you a clear picture.
Let me check the project's root directory first.
Analyzed -> `~/Code/next-supabase-saas-kit-turbo`
Let me check the main web application structure:
Analyzed-> `apps/web`
Based on the project structure and guidelines, here's how the project is organized and where you should add new features:
1. Main Application Structure (/apps/web/):
- `/app` - Next.js App Router pages and routes
- `/components` - Global reusable components
- `/config` - Global configuration files
- `/lib` - Utility functions and shared logic
- `/content` - Markdoc content
- `/supabase` - Supabase configuration and types
2. Route Organization (/apps/web/app/):
- `/home` - Protected routes
- `/(user)` - Personal workspace routes
- `/[account]` - Team workspace routes
- `/(marketing)` - Public marketing pages
- `/auth` - Authentication pages
Where to Add New Features:
1. New Pages:
- Protected user features → `/app/home/(user)`
- Team features → `/app/home/[account]`
- Public pages → `/app/(marketing)`
- Auth-related → `/app/auth`
2. New Components:
- Global components → `/components`
- Page-specific components → Co-locate with the page in /app
3. Database Operations:
- Server Actions → `_lib/server/server-actions.ts`
- API Routes → `/app/api`
4. Shared Logic:
- Utilities → `/lib`
- Types → `/lib/types`
- Hooks → `/lib/hooks`
5. Key Development Patterns:
- Use Server Components by default
- Client Components when needed (marked with 'use client')
- Server Actions for mutations
- React Query for client-side data fetching
- Shadcn UI for components
- Zod for schema validation
- TypeScript for type safety
- Shared Packages (/packages):
- UI components → `@kit/ui`
- Shared utilities → `@kit/shared`
When adding new features, remember to:
- Follow the TypeScript guidelines (strict typing, no explicit returns)
- Keep components small and focused
- Use Server Components where possible
- Handle errors appropriately
- Add proper RLS policies for database operations
- Include i18n support with withI18n
- Add appropriate metadata for pages

The answer above is great! You can drill down into more specific sections. I find that despite the limited rules, Windsurf does a very good job at being able to analyze the structure of the code and provide a more detailed response.

Q2. Where can I update the sidebar of the personal account?

I asked Windsurf to help me update the sidebar of the personal account:

Where can I update the sidebar of the personal account?

Here's the response:

Let me help you find the sidebar component for the personal account. Since this would be part of the user workspace layout, I'll look in the appropriate directories.
Searched codebase: 70 results
Let me search for the navigation configuration since that's where the sidebar items are defined:
Searched codebase: 25 results
Let me look at the personal account navigation config file:
Analyzed: personal-account-navigation.config.tsx:(1-101)
Based on my analysis, here's where you can update the sidebar for the personal account:
1. The main configuration file is at `/apps/web/config/personal-account-navigation.config.tsx`
2. The sidebar items are defined in the routes array, which currently has two main sections:
- Application section with Home
- Settings section with Profile and optional Billing
3. Each route item can have:
- `label`: The display text (using i18n keys)
- `path`: The URL path
- `Icon`: A Lucide React icon component
- `end`: Boolean to indicate exact path matching
- `children`: Array of sub-routes
To update the sidebar, you would modify the routes array in this file. For example, to add a new item, you would add a new object to the appropriate section's children array.
Would you like me to show you how to add a specific item to the sidebar?

Wow! That was an incredible answer. After analyzing the codebase, Windsurf was able to find the configuration file at ``/apps/web/config/personal-account-navigation.config.tsx`.

After analyzing the configuration file, Windsurf also provided a correct definition of the object. It was able to understand the context and provide the correct code.

This was actually a lot better than what Cursor was able to do.

Q3. How would you modify the configuration to add a new route named Bookmarks?

I then proceeded to ask Windsurf to help me with the following prompt:

How would you modify the configuration to add a new route named Bookmarks?

Windsurf responded with the following code:

Every snippet was correct and allowed me to apply each change separately. I loved both the answer's correctness and the UX. Well done!

Database: building a Postgres schema with Windsurf

Creating a database schema is like architecting the foundation of your house - get it wrong, and everything built on top becomes unstable. We need to ensure our schema is:

  • Secure and properly access-controlled
  • Performant under load
  • Consistent with Makerkit's patterns
  • Easy to maintain and extend

Learning Goals 🎯:

By the end of this section, you'll learn:

  • How to effectively prompt AI for schema design
  • What makes a good database schema
  • How to validate AI-generated schemas
  • Common pitfalls to avoid

1. First Attempt: The Naive Approach

My first attempt was simple but too vague:

I want to start writing the required database schema for supporting the app's functionality. Brainstorm the schema with me.

Result: While Windsurf provided decent output and correctly analyzed the existing schema without me providing any context, it didn't reuse any of the existing functions in the existing schema, which already provide access control and other features.

In addition, it didn't add any indexes and necessary constraints to the schema.

Let's refine the prompt next.

2. Second Attempt: The Refined Prompt

Learning from this, I crafted a more detailed prompt:

I want to start writing the required database schema for supporting the app's functionality. Brainstorm the schema with me.
Requirements:
1. Only include new tables needed for app functionality
2. Team accounts own tables (not individual users)
3. Ensure proper team member access control
4. Include appropriate indexes
5. Define table relationships
6. Consider performance implications
7. Ensure schema safety
8. Maintain consistency
9. Reuse existing schema functions for access control
10. Reference accounts table (not auth.users)

In the prompt above, I've added some additional requirements to ensure the schema is secure, performant, consistent, and easy to maintain

  1. Focus: The AI may want to try create tables that exist (users, accounts, etc.). Best to avoid this.
  2. Accounts: You want to rarely use the auth.users table, and instead use the public.accounts table
  3. Permissions: You want to use public.has_role_on_account to check permissions, a function built-in to Makerkit.
  4. Indexes: You want to include appropriate indexes for performance
  5. Relationships: You want to define table relationships, such as public.accounts to public.accounts_memberships
  6. Performance: You want to consider performance implications, such as adding appropriate indexes

💡 Key Insight: By providing Makerkit's existing SQL schema as context, the AI could better understand and reuse existing patterns. When creating a a DB schema, always provide the main migration file as context to the AI.

The output above is a lot more detailed and includes the necessary constraints and triggers for the schema. It also reuses existing functions for access control such as has_role_on_account.

Result of the Refined Prompt

Windsurf's output was great this time! Let's look at what it got right:

  • ✅ Account Ownership: Correctly linked tables to public.accounts
  • ✅ Access Control: Used public.has_role_on_account for permissions
  • ✅ Security: Created proper RLS policies
  • ✅ Automation: Added helpful triggers for timestamps
  • ✅ Performance: Included appropriate indexes
-- Bookmarks table - owned by team accounts
create table public.bookmarks (
id uuid primary key default gen_random_uuid(),
account_id uuid not null references public.accounts(id) on delete cascade,
title text not null,
url text not null check (char_length(url) <= 2048),
description text,
favicon_url text,
created_at timestamptz default now(),
updated_at timestamptz default now()
);
-- Collections for organizing bookmarks
create table public.collections (
id uuid primary key default gen_random_uuid(),
account_id uuid not null references public.accounts(id) on delete cascade,
name text not null check (char_length(name) <= 100),
description text,
parent_id uuid references public.collections(id) on delete cascade,
created_at timestamptz default now(),
updated_at timestamptz default now()
);
-- Bookmarks to Collections relationship
create table public.collection_bookmarks (
collection_id uuid not null references public.collections(id) on delete cascade,
bookmark_id uuid not null references public.bookmarks(id) on delete cascade,
created_at timestamptz default now(),
primary key (collection_id, bookmark_id)
);
-- Tags for categorization
create table public.bookmark_tags (
id uuid primary key default gen_random_uuid(),
account_id uuid not null references public.accounts(id) on delete cascade,
name text not null check (char_length(name) <= 50),
created_at timestamptz default now(),
unique(account_id, name)
);
-- Junction table for bookmarks and tags
create table public.bookmark_tag_relations (
bookmark_id uuid not null references public.bookmarks(id) on delete cascade,
tag_id uuid not null references public.bookmark_tags(id) on delete cascade,
created_at timestamptz default now(),
primary key (bookmark_id, tag_id)
);
-- Indexes for performance
create index idx_bookmarks_account_id on public.bookmarks(account_id);
create index idx_bookmarks_created_at on public.bookmarks(created_at);
create index idx_collections_account_id on public.collections(account_id);
create index idx_collections_parent_id on public.collections(parent_id);
create index idx_bookmark_tags_account_id on public.bookmark_tags(account_id);
create index idx_bookmark_tags_name on public.bookmark_tags(name);
-- Enable RLS
alter table public.bookmarks enable row level security;
alter table public.collections enable row level security;
alter table public.collection_bookmarks enable row level security;
alter table public.bookmark_tags enable row level security;
alter table public.bookmark_tag_relations enable row level security;
-- RLS Policies using existing functions
-- Bookmarks policies
create policy "Team members can read bookmarks" on public.bookmarks
for select to authenticated
using (public.has_role_on_account(account_id));
create policy "Team members can manage bookmarks" on public.bookmarks
for all to authenticated
using (public.has_role_on_account(account_id));
-- Collections policies
create policy "Team members can read collections" on public.collections
for select to authenticated
using (public.has_role_on_account(account_id));
create policy "Team members can manage collections" on public.collections
for all to authenticated
using (public.has_role_on_account(account_id));
-- Bookmark tags policies
create policy "Team members can read tags" on public.bookmark_tags
for select to authenticated
using (public.has_role_on_account(account_id));
create policy "Team members can manage tags" on public.bookmark_tags
for all to authenticated
using (public.has_role_on_account(account_id));
-- Junction tables policies
create policy "Team members can manage collection bookmarks" on public.collection_bookmarks
for all to authenticated
using (
exists (
select 1 from public.collections c
where c.id = collection_id
and public.has_role_on_account(c.account_id)
)
);
create policy "Team members can manage bookmark tags" on public.bookmark_tag_relations
for all to authenticated
using (
exists (
select 1 from public.bookmarks b
where b.id = bookmark_id
and public.has_role_on_account(b.account_id)
)
);

Validating the Database schema using Tests

You can also ask Windsurf to bootstrap testing cases for you to validate the database schema. pgTap is a popular testing framework for Postgres, and we already use it in Makerkit. This gives Windsurf a lot of information to work with.

This can be done by adding the following to the prompt:

Can you create some pgTap tests for this schema?

As Context I provided it with two files:

  1. Schema: the migration file generated by the AI
  2. Tests example: an existing pgTap test file that tests other parts of the schema that Windsurf can use as a reference
Windsurf Tests

You can then ask Windsurf to apply the changes and add the test file to tests directory. Makerkit will then be able to run the tests and ensure the database schema is valid according to the tests.

Please make sure to add more test cases so that the database schema is validated also by yourself.

Building Pages with Windsurf

Now that we have a database schema, we can start building the pages that will support the app's functionality. In this section, we will learn how to build a page with Windsurf.

Understanding the User vs the Team contexts

Makerkit has two contexts for the user and the team. The user context is used for personal accounts, and the team context is used for team accounts.

It is extremely important to always tell Windsurf which context you are working with. This will help Windsurf understand the context and provide the most relevant information.

Asking Windsurf to create a page

When starting functionalities from scratch, I found it's important to go step by step, instead of big bang approaches.

I asked Windsurf to create a page for me:

I want to create a new page under the team workspace named "Bookmarks". Please reuse the same layout as the other pages under the same workspace. Do not add any functionality yet. Use the correct metadata.

Windsurf responded with the following code:

import { use } from 'react';
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
import { PageBody } from '@kit/ui/page';
import { Trans } from '@kit/ui/trans';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
import { TeamAccountLayoutPageHeader } from '../_components/team-account-layout-page-header';
type BookmarksPageProps = {
params: Promise<{ account: string }>;
}
export const generateMetadata = async () => {
const i18n = await createI18nServerInstance();
const title = i18n.t('teams:bookmarks.pageTitle');
return {
title,
};
};
function BookmarksPage({ params }: BookmarksPageProps) {
const account = use(params).account;
return (
<>
<TeamAccountLayoutPageHeader
account={account}
title={<Trans i18nKey={'common:routes.bookmarks'} />}
description={<AppBreadcrumbs />}
/>
<PageBody>
{/* Content will be added later */}
</PageBody>
</>
);
}
export default withI18n(BookmarksPage);

This was absolutely flawless! 🎉 I'm quite impressed by how Windsurf was able to correctly analyze the existing pages and provide the correct code.

Since the page is not yet within the sidebar, I asked Windsurf to update the sidebar configuration:

Can you add the Bookmarks route to team navigation configuration?

Windsurf blew me away by how well it analyzed multiple times the code and finally provided the correct code:

Windsurf Bookmarks Page

Now the page looks like this, still empty but with the correct metadata:

Windsurf Bookmarks Page

Windsurf followed up asking me whether I wanted to add the translation keys, to which I happily agreed. Windsurf correctly added a translation key to the common.json file, where we store the translations for the common strings used in the app. Incredible!

Data Fetching

Now that we have the page created, we want to ask Windsurf to help us with the data fetching items from the Supabase database and display them in the UI.

Let's get started by asking Windsurf how we can fetch the data from the Supabase from our newly created bookmarks table. For the time being, I only want to understand how to fetch the data and how Windsurf can help me with that.

Therefore I start a fresh new chat with Windsurf:

I want to fetch the data from the bookmarks table and display it in the UI. How can we use the Supabase Client to fetch the data?

Windsurf responded with the majority of the code being correct, but made a crucial mistake: it interpreted the account parameter as an ID of the account, but it's actually a slug. Therefore, it filtered the data using a wrong column name. Cursor made the same mistake.

After asking it to fix the mistake, Windsurf provided the following code:

import { use } from 'react';
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
import { PageBody } from '@kit/ui/page';
import { Trans } from '@kit/ui/trans';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { TeamAccountLayoutPageHeader } from '../_components/team-account-layout-page-header';
type BookmarksPageProps = {
params: Promise<{ account: string }>;
}
async function getBookmarks(accountSlug: string) {
const client = getSupabaseServerClient();
const { data, error } = await client
.from('bookmarks')
.select(`
*,
accounts!inner (*)
`)
.eq('accounts.slug', accountSlug);
if (error) {
throw error;
}
return data ?? [];
}
export const generateMetadata = async () => {
const i18n = await createI18nServerInstance();
const title = i18n.t('teams:bookmarks.pageTitle');
return {
title,
};
};
function BookmarksPage({ params }: BookmarksPageProps) {
const account = use(params).account;
const bookmarks = use(getBookmarks(account));
return (
<>
<TeamAccountLayoutPageHeader
account={account}
title={<Trans i18nKey={'common:routes.bookmarks'} />}
description={<AppBreadcrumbs />}
/>
<PageBody>
{/* Content will be added later */}
</PageBody>
</>
);
}
export default withI18n(BookmarksPage);

In a few places, Windsurf made some mistakes with the imports. After highlighting the mistakes, Windsurf was able to get back on the right track and fix its mistakes. It's very important to verify the output at every step to ensure that the code is correct and starting from a clean slate.

Improving the code

Windsurf's code above is functionally correct (like Cursor's), however, it is not following Makerkit's conventions. We can ask Windsurf to help us improve the code by separating the concerns of fetching data and displaying it in the UI.

For example, we can ask them to follow Makerkit's conventions and create a service in _lib/server/bookmarks/bookmarks.service.ts to fetch the data and use the service in the Server Component to fetch the data and pass it to the Client Component.

This will help us keep the code clean and maintainable.

Not necessary, but helps in the long run.

Displaying the Data in the UI

Now that we have the data fetched, we can display it in the UI. Using Windsurf's write mode, I asked to display the data in a list of bookmarks:

I want to display the data in a list of bookmarks

Windsurf provided the following code:

import { use } from 'react';
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
import { PageBody } from '@kit/ui/page';
import { Trans } from '@kit/ui/trans';
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
import Link from 'next/link';
import { ExternalLink } from 'lucide-react';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { TeamAccountLayoutPageHeader } from '../_components/team-account-layout-page-header';
type BookmarksPageProps = {
params: Promise<{ account: string }>;
}
async function getBookmarks(accountSlug: string) {
const client = getSupabaseServerClient();
const { data, error } = await client
.from('bookmarks')
.select(`
*,
accounts!inner (*)
`)
.eq('accounts.slug', accountSlug);
if (error) {
throw error;
}
return data ?? [];
}
export const generateMetadata = async () => {
const i18n = await createI18nServerInstance();
const title = i18n.t('teams:bookmarks.pageTitle');
return {
title,
};
};
function BookmarksList({ bookmarks }: { bookmarks: Array<{ title: string; url: string }> }) {
if (!bookmarks.length) {
return (
<Card>
<CardContent className="py-8 text-center text-sm text-muted-foreground">
<Trans i18nKey="teams:noBookmarks" />
</CardContent>
</Card>
);
}
return (
<Card>
<CardHeader>
<CardTitle>
<Trans i18nKey="teams:bookmarks" />
</CardTitle>
</CardHeader>
<CardContent className="grid gap-4">
{bookmarks.map((bookmark) => (
<div key={bookmark.url} className="flex items-center justify-between">
<span className="font-medium">{bookmark.title}</span>
<Link
href={bookmark.url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground"
>
{bookmark.url}
<ExternalLink className="h-4 w-4" />
</Link>
</div>
))}
</CardContent>
</Card>
);
}
function BookmarksPage({ params }: BookmarksPageProps) {
const account = use(params).account;
const bookmarks = use(getBookmarks(account));
return (
<>
<TeamAccountLayoutPageHeader
account={account}
title={<Trans i18nKey={'common:routes.bookmarks'} />}
description={<AppBreadcrumbs />}
/>
<PageBody>
<BookmarksList bookmarks={bookmarks} />
</PageBody>
</>
);
}
export default withI18n(BookmarksPage);

The code above seems correct, but I cannot test it until we have a bookmark in the page. Let's go ahead and create a form to add a new bookmark.

Building Forms with Windsurf

Now that we have a list of bookmarks, we can create a form to add a new bookmark.

I asked Windsurf to help me with the following prompt:

I now want to add a button that opens a Dialog. The Dialog will contain a form that allows us to insert a bookmark in the DB using a Server Action. Please use a separate client component for the Dialog and the Form.

After some back and fort on some minor issues (which I resolved by optimizing the rules) - I got the following result.

The page was changed to include a button to create a new bookmark. The button is only visible to the team owner and the button opens a dialog that allows the user to enter the URL and the title of the bookmark.

Windsurf Bookmark Button

Adding Bookmarks with a Form

The below is the output of the Windsurf rule that I used to add the button to create a new bookmark. I added the CreateBookmarkDialog component to the BookmarksPage component.

'use client';
import { useTransition } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { toast } from 'sonner';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@kit/ui/form';
import { Input } from '@kit/ui/input';
import { Textarea } from '@kit/ui/textarea';
import { Button } from '@kit/ui/button';
import { BookmarkSchema, type BookmarkFormData } from '../bookmark.schema';
import { createBookmarkAction } from '../_lib/server/server-actions';
export function BookmarkForm({ onSuccess }: { onSuccess?: () => void }) {
const [pending, startTransition] = useTransition();
const form = useForm<BookmarkFormData>({
resolver: zodResolver(BookmarkSchema),
defaultValues: {
title: '',
url: '',
description: '',
favicon_url: '',
},
});
function onSubmit(data: BookmarkFormData) {
startTransition(async () => {
try {
await createBookmarkAction(data);
form.reset();
toast.success('Bookmark created successfully');
onSuccess?.();
} catch (error) {
toast.error('Failed to create bookmark');
}
});
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>Title</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="url"
render={({ field }) => (
<FormItem>
<FormLabel>URL</FormLabel>
<FormControl>
<Input {...field} type="url" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="favicon_url"
render={({ field }) => (
<FormItem>
<FormLabel>Favicon URL</FormLabel>
<FormControl>
<Input {...field} type="url" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={pending}>
{pending ? 'Creating...' : 'Create Bookmark'}
</Button>
</form>
</Form>
);
}

Adding the Dialog

The Bookmark form is now wrapped in a CreateBookmarkDialog component that we can use to display the dialog.

'use client';
import { useState } from 'react';
import { Plus } from 'lucide-react';
import { Button } from '@kit/ui/button';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@kit/ui/dialog';
import { BookmarkForm } from './bookmark-form';
export function CreateBookmarkDialog() {
const [open, setOpen] = useState(false);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button>
<Plus className="h-4 w-4 mr-2" />
Add Bookmark
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Create New Bookmark</DialogTitle>
</DialogHeader>
<BookmarkForm onSuccess={() => setOpen(false)} />
</DialogContent>
</Dialog>
);
}

Finally, Windsurf updated the BookmarksPage component to include the CreateBookmarkDialog component:

import { use } from 'react';
import Link from 'next/link';
import { ExternalLink } from 'lucide-react';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
import { PageBody } from '@kit/ui/page';
import { Trans } from '@kit/ui/trans';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
import { TeamAccountLayoutPageHeader } from '../_components/team-account-layout-page-header';
import { CreateBookmarkDialog } from './_components/create-bookmark-dialog';
type BookmarksPageProps = {
params: Promise<{ account: string }>;
};
async function getBookmarks(accountSlug: string) {
const client = getSupabaseServerClient();
const { data, error } = await client
.from('bookmarks')
.select(
`
*,
accounts!inner (*)
`,
)
.eq('accounts.slug', accountSlug);
if (error) {
throw error;
}
return data ?? [];
}
export const generateMetadata = async () => {
const i18n = await createI18nServerInstance();
const title = i18n.t('teams:bookmarks.pageTitle');
return {
title,
};
};
function BookmarksList({
bookmarks,
}: {
bookmarks: Array<{ title: string; url: string }>;
}) {
if (!bookmarks.length) {
return (
<Card>
<CardContent className="py-8 text-center text-sm text-muted-foreground">
<Trans i18nKey="teams:noBookmarks" />
</CardContent>
</Card>
);
}
return (
<Card>
<CardHeader>
<CardTitle>
<Trans i18nKey="teams:bookmarks.title" />
</CardTitle>
</CardHeader>
<CardContent className="grid gap-4">
{bookmarks.map((bookmark) => (
<div key={bookmark.url} className="flex items-center justify-between">
<span className="font-medium">{bookmark.title}</span>
<Link
href={bookmark.url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground"
>
{bookmark.url}
<ExternalLink className="h-4 w-4" />
</Link>
</div>
))}
</CardContent>
</Card>
);
}
function BookmarksPage({ params }: BookmarksPageProps) {
const account = use(params).account;
const bookmarks = use(getBookmarks(account));
return (
<>
<TeamAccountLayoutPageHeader
account={account}
title={<Trans i18nKey={'common:routes.bookmarks'} />}
description={<AppBreadcrumbs />}
>
<CreateBookmarkDialog />
</TeamAccountLayoutPageHeader>
<PageBody>
<Card>
<CardHeader>
<CardTitle>
<Trans i18nKey="teams:bookmarks.title" />
</CardTitle>
</CardHeader>
<CardContent>
<BookmarksList bookmarks={bookmarks} />
</CardContent>
</Card>
</PageBody>
</>
);
}
export default withI18n(BookmarksPage);

Server Actions

Windsurf also included the correct Server Actions to handle the form submission. The createBookmarkAction function is used to create a new bookmark.

'use server';
import { enhanceAction } from '@kit/next/actions';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { BookmarkSchema } from '../../bookmark.schema';
export const createBookmarkAction = enhanceAction(
async function (data) {
const client = getSupabaseServerClient();
const { error } = await client
.from('bookmarks')
.insert(data);
if (error) throw error;
return { success: true };
},
{
auth: true,
schema: BookmarkSchema,
}
);

Revalidating the Form

At first, Winfsurf didn't include a way to revalidate the page after a submission. In order to show the updated bookmarks list, we need to revalidate the page.

We can do this by using the revalidatePath function from next/cache. I asked Windsurf to solve this issue:

How can I revalidate the page after a successful submission?

Windsurf updated the Server Action to include the revalidatePath function:

'use server';
import { revalidatePath } from 'next/cache';
import { enhanceAction } from '@kit/next/actions';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { BookmarkSchema } from '../../bookmark.schema';
export const createBookmarkAction = enhanceAction(
async function (data) {
const client = getSupabaseServerClient();
const { error } = await client
.from('bookmarks')
.insert(data);
if (error) throw error;
revalidatePath('/home/[account]/bookmarks', 'page');
return { success: true };
},
{
auth: true,
schema: BookmarkSchema,
}
);

Our form is now successfully working, and we can now display bookmarks in the UI. Yay! 🎉

Summarizing the Best Practices of using Windsurf with Makerkit

Okay, we've finished this little tutorial. Here's what we've achieved with Windsurf, in a little more than a few minutes:

  1. Database Schema: we've learned how to effectively prompt AI for schema design
  2. Building Pages: we've learned how to build a page with Windsurf
  3. Data Fetching: we've learned how to fetch data from Supabase using Windsurf
  4. Building Forms: we've learned how to build a form with Windsurf
  5. Improving Code: we've learned how to improve the code using Windsurf
  6. Fixing Bugs: we've learned how to fix bugs using Windsurf

1. One task at a time

One thing that is quite important to remember is that isolating the task at hand in small chunks is the key to success when working with AI. Try to focus your prompts on achieving small tasks at a time, and then gradually build on top of them.

2. Use Context files

Windsurf did a better job than Cursor at being able to find the correct files. However, it's not perfect. Sometimes, it's your job to help it find the correct files.

The most important thing to remember is to use context files. Context files are files that contain information about the project, such as the database schema, the UI components, and the translations.

By helping Windsurf with the correct context files, you will reduce the amount of allucinations and errors Windsurf will (quite inevitably) make.

3. Avoid Context Pollution

Sometimes, remember to restart new chat sessions with Windsurf to avoid context pollution. This is probably a valid point for any AI tool.

4. Update the rules after a manual edit

After making a manual edit to the rules, you can update the rules to drive Windsurf to generate the correct output.

About Makerkit, the best Next.js SaaS Boilerplate

Makerkit is a full-stack SaaS template that gives you a foundation to build ambitious B2B SaaS applications.