# Best Practices for Building a SaaS with Windsurf and Makerkit (2026)

> Build production SaaS faster with Windsurf and Makerkit. Updated for 2026 with Cascade, AGENTS.md rules, MCP, and Next.js 16 patterns.

*Published: 2025-01-17*
*Canonical: https://makerkit.dev/blog/tutorials/build-saas-windsurf*

---

In this post, I want to show you the best practices to build a SaaS with Windsurf and Makerkit. [Windsurf](https://windsurf.com) is an AI-native editor whose flagship feature, **Cascade**, runs multi-step agentic edits across your codebase with full project awareness.

{% alert type="info" title="Updated for 2026" %}
This guide has been refreshed for Windsurf's current state: the **Cognition acquisition** (mid-2025), the move from `codeium.com` to `windsurf.com`, **Cascade** agent mode, **MCP server support**, and the standardization on **`AGENTS.md`** as the project rules format. For a head-to-head against other AI editors, see [Best Vibe Coding Tools for SaaS Development](/blog/saas/best-vibe-coding-tools).
{% /alert %}

{% img src="/images/posts/windsurf-workspace.webp" alt="Windsurf Workspace"
width="3600" height="2256" class="border" /%}

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](/blog/tutorials/build-saas-cursor), 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 🎯

New to Makerkit? Start with the [free Makerkit course](/courses/nextjs-turbo/introduction) to learn the fundamentals.

- 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

Windsurf reads project rules from a few sources, in order of preference:

1. **`AGENTS.md`** at the repository root — the modern standard, shared across Windsurf, Cursor, Claude Code, and Codex
2. **`.windsurf/rules/*.md`** — scoped rule files attached based on activation modes (always, manual, model-decision, glob)
3. **`.windsurfrules`** (legacy) — still read, but `AGENTS.md` is what we maintain

Makerkit ships `AGENTS.md` files at the repo root, in `apps/web`, in `apps/web/supabase`, and inside each `packages/*`. Cascade pulls the right context based on the file you're editing. The rules cover:

- Project structure and conventions
- Common patterns and components (`enhanceAction`, `getSupabaseServerClient`, `Trans`)
- Import paths and aliases (`@kit/ui`, `@kit/supabase`, `@kit/next`)
- Type definitions and schemas

💡 **Pro Tip**: Keep `AGENTS.md` updated with your SaaS business logic. The character limits on `.windsurfrules` that constrained the original version of this guide are mostly historical — Cascade now reads `AGENTS.md` files dynamically as it navigates the repo, so you no longer have to ration context.

### Cascade and MCP

Cascade is what makes Windsurf worth choosing for serious feature work. It runs autonomously across files, explains each step, and pauses for review at decision points. Pair it with the [Makerkit MCP server](/blog/changelog/mcp-server-v2) so Cascade can query your live Supabase schema and translations directly:

```json title=".windsurf/mcp_config.json"
{
  "mcpServers": {
    "makerkit": {
      "command": "npx",
      "args": ["-y", "@makerkit/mcp"]
    }
  }
}
```

[👉 Learn more about the Windsurf rules for Makerkit.](/docs/next-supabase-turbo/installation/llms#windsurf-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:

````md
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:

{% img src="/images/posts/windsurf-create-bookmark-route.webp" width="1000" height="500" class="border" /%}

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:

```markdown
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

```sql
-- 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

{% img src="/images/posts/windsurf-pgtap-tests.webp" alt="Windsurf Tests" width="1000" height="500" /%}

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:

```tsx
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?

{% alert type="info" title="Make sure to mention the correct context" %}
Remember to specify which context you should be adding the route to. In this
case, it's the team context.
{% /alert %}

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

{% img src="/images/posts/windsurf-update-sidebar-config.webp" alt="Windsurf Bookmarks Page" width="1000" height="500" /%}

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

{% img src="/images/posts/windsurf-empty-bookmarks-page.webp" alt="Windsurf Bookmarks Page" width="1000" height="500" /%}

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:

```tsx
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:

```tsx title="app/home/[account]/bookmarks/page.tsx"
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.

{% img src="/images/posts/windsurf-create-bookmark-dialog.webp" alt="Windsurf Bookmark Button" width="1000" height="500" /%}

#### 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.

```tsx title="app/home/[account]/bookmarks/_components/bookmark-form.tsx"
'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.

```tsx title="app/home/[account]/bookmarks/_components/create-bookmark-dialog.tsx"
'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:

```tsx title="app/home/[account]/bookmarks/page.tsx"
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.

```tsx title="app/home/[account]/bookmarks/_lib/server/server-actions.ts"
'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:

```tsx title="app/home/[account]/bookmarks/_lib/server/server-actions.ts"
'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 `AGENTS.md` or a `.windsurf/rules/*.md` file, commit it. Cascade reloads rules on the next prompt, and your teammates inherit the same context for free.

### 5. Use the MCP server for grounded answers

The Makerkit MCP server lets Cascade query the live Supabase schema, list translations, and inspect existing route handlers. Grounded context beats hallucination — once MCP is wired up, prompts like "add a `tags` table that mirrors how `bookmarks` is set up" produce migrations that match the real schema, not a plausible guess.

## What Changed in 2026

If you used this guide when it was first published in early 2025, here's what's new:

- **Cognition acquired Windsurf** in mid-2025 — the brand moved from `codeium.com` to `windsurf.com` and the product roadmap accelerated
- **`AGENTS.md` replaces `.windsurfrules`** as the maintained convention — same file is read by Windsurf, Cursor, Claude Code, and Codex
- **Cascade** is now the default mode for non-trivial work, with multi-file edits and step-by-step review
- **MCP servers** ground Cascade's responses in live data — schemas, files, translations — instead of static rules
- **Next.js 16** ships an awaited `params` Promise — the code samples in this guide already reflect that pattern

## 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. Pair it with [Windsurf](https://windsurf.com), [Cursor](/blog/tutorials/build-saas-cursor), or [Claude Code](/blog/tutorials/claude-code-best-practices) to ship production features in days, not months. Once your build is stable, [schedule Claude Code routines](/blog/tutorials/claude-code-routines-guide) to handle recurring work in the cloud.
