Skip to main content

Adding Features

This guide shows how to add new features following the conventions used throughout the codebase.

Directory Conventions

LayerLocationContains
Pagesapp/(platform)/your-feature/Next.js App Router pages
API routesapp/api/your-feature/Route handlers
Server actionslib/your-feature/actions.ts"use server" functions
Typeslib/your-feature/types.tsTypeScript types
Context/hookslib/your-feature/context.tsxReact context + hooks
UI componentscomponents/your-feature/Feature-specific components
Migrationssupabase/migrations/Timestamped SQL files

Migration Naming

Use a timestamp prefix that puts your migrations after the template’s own migrations:
supabase/migrations/20260401000000_create_projects.sql
The template’s migrations use dates in the 2026XXXX range. Use a date after the latest existing migration to ensure correct ordering.

Example: Adding a “Projects” Feature

1

Create the migration

-- supabase/migrations/20260401000000_create_projects.sql
CREATE TABLE projects (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
  name text NOT NULL CHECK (char_length(name) BETWEEN 1 AND 100),
  description text,
  created_by uuid REFERENCES auth.users(id),
  created_at timestamptz DEFAULT now(),
  updated_at timestamptz DEFAULT now()
);

ALTER TABLE projects ENABLE ROW LEVEL SECURITY;

CREATE POLICY "workspace members can access projects"
  ON projects FOR ALL
  USING (is_workspace_member(workspace_id));
2

Generate updated TypeScript types

pnpm run types:generate
This regenerates types/database.types.ts from your Supabase schema.
3

Create server actions

// lib/projects/actions.ts
"use server";

import { createServerClient } from "@/lib/supabase/server";

export async function createProject(workspaceId: string, name: string) {
  const supabase = await createServerClient();
  const { data: { user } } = await supabase.auth.getUser();
  if (!user) return { error: "Not authenticated" };

  const { data, error } = await supabase
    .from("projects")
    .insert({ workspace_id: workspaceId, name, created_by: user.id })
    .select()
    .single();

  if (error) return { error: error.message };
  return { project: data };
}
4

Add a page

// app/(platform)/projects/page.tsx
import { createServerClient } from "@/lib/supabase/server";
import { PageContainer } from "@/components/ui/page-container";

export default async function ProjectsPage() {
  const supabase = await createServerClient();
  const { data: projects } = await supabase
    .from("projects")
    .select("*")
    .order("created_at", { ascending: false });

  return (
    <PageContainer size="default">
      <h1>Projects</h1>
      {/* render projects */}
    </PageContainer>
  );
}
5

Add to sidebar navigation

// saas.config.ts
import { FolderOpen } from "lucide-react";

sidebar: {
  navItems: [
    { href: "/projects", label: "Projects", icon: <FolderOpen /> },
  ],
},

Caching Considerations

For workspace-scoped data protected by RLS, do not use unstable_cache(). It caches at the process level and bypasses RLS on cache hits, which can leak data across users. Instead, use React cache() for request-scoped deduplication:
import { cache } from "react";

export const cachedGetProject = cache(async (id: string) => {
  const supabase = await createServerClient();
  const { data } = await supabase.from("projects").select("*").eq("id", id).single();
  return data;
});
For cache invalidation after mutations, use revalidateTag() with tag constants in lib/cache/tags.ts.

Pulling Template Updates

If the template receives upstream fixes, use sync-upstream.sh to pull changes into your fork while preserving your domain code:
./sync-upstream.sh
The script merges template changes onto a separate branch so you can review diffs before applying them to your main branch.