This starter template is designed for developers looking to build modern web applications with powerful AI-assisted IDE tools like Cursor & WindSurf. It's perfect for those transitioning away from no-code/low-code platforms like Lovable and v0, who want more control and flexibility over their stack.
We are committed to regularly updating this project with new features, patterns, and best practices. Our goal is to help developers learn these powerful tools and technologies through practical, real-world examples that you can build upon.
- Frontend: Next.js 15 with App Router, Tailwind CSS, and shadcn/ui components
- Backend: Supabase for authentication, database, and storage
- Billing: Complete Stripe integration with webhook handling
- Local Development: Full local development setup with Docker
- AI-Ready: MCP (Model Context Protocol) configurations for AI-assisted development
This template shows you how to build a complete SaaS application with authentication, billing, and database functionality without the limitations of no-code platforms.
Before starting, you need to have the following:
- GitHub Account (for repository management)
- Git (for version control)
- Node.js (v16 or higher)
- Docker Desktop (includes Docker CLI)
- Stripe CLI (for Stripe integration)
- Stripe Account (for payment processing)
If you don't have these tools installed, follow these instructions:
-
Create a GitHub Account:
- Go to GitHub's signup page and follow the instructions
-
Install Git:
- Download and install from Git website
- Verify installation:
git --version
-
Install Node.js:
- Download and install from Node.js website (LTS version recommended)
- Verify installation:
node --version && npm --version
-
Install Docker Desktop:
- Download and install from Docker Desktop website
- Windows/Mac: Follow the installer instructions
- Verify installation:
docker --version - Make sure Docker Desktop is running before starting Supabase
-
Install Stripe CLI:
- Windows:
- Download the zip file
stripe_1.26.1_windows_x86_64.zipfrom Stripe CLI Releases - Extract it to a folder on your computer
- Add the folder to your PATH environment variable:
- Copy the full path to the folder where you extracted the stripe.exe file (right-click on the stripe file in File Explorer, hold Shift and select "Copy as path")
- Press Windows key + X and select "System"
- Click on "Advanced system settings" on the right
- Click the "Environment Variables" button near the bottom
- In the "System variables" section, find and select "Path"
- Click "Edit"
- Click "New"
- Paste the folder path you copied earlier (without the filename itself and without .exe, just the folder path)
- Click "OK" on all dialog boxes to save changes
- Verify installation: Open a new Command Prompt or PowerShell window and run
stripe --version
- Download the zip file
- macOS: Use
brew install stripe/stripe-cli/stripe - Linux: Download the appropriate package from Stripe CLI Releases
- Verify installation:
stripe --version
- Windows:
-
Create a Stripe Account:
- Sign up at Stripe's registration page
- No banking information is required for test mode
Note: We'll be using npx to run Supabase CLI commands instead of installing it globally. This ensures:
- Everyone uses the same CLI version
- No global dependencies to manage
- No conflicts with other Supabase projects
- Easier setup process
Follow this step-by-step workflow to set up the complete project:
All Supabase and CLI commands should be executed using npx instead of globally installed tools. This ensures:
- Everyone uses the same CLI version
- No global dependencies to manage
- No conflicts with other Supabase projects
- Easier setup process
# β
DO THIS:
npx supabase start
npx supabase migration up
# β NOT THIS:
supabase start
supabase migration upAlways add the --debug flag when troubleshooting Supabase issues:
# Supabase management commands with debug flag
npx supabase start --debug # Start all Supabase services with detailed logging
npx supabase stop --debug # Stop all Supabase services with detailed logging
npx supabase status --debug # Show status of local Supabase services with detailed info
# Migration commands with debug flag
npx supabase migration up --debug # Apply pending migrations with detailed logging
npx supabase migration new name --debug # Create a new migration with verbose output
npx supabase migration list --debug # List all migrations with detailed status
npx supabase migration repair --debug # Repair migrations state with detailed logging
# Database commands with debug flag
npx supabase db reset --debug # Reset database to clean state with detailed logging
npx supabase db push --debug # Push schema changes to remote database with detailed logs
npx supabase db pull --debug # Pull schema changes from remote database with details
npx supabase db diff --debug # Show diff between local and remote database with details
npx supabase db lint --debug # Check database for issues with detailed reporting
npx supabase db dump --debug # Dump database schema with verbose output
# Gen commands with debug flag
npx supabase gen types typescript --debug # Generate TypeScript types with detailed output
npx supabase gen enums typescript --debug # Generate enums with detailed logs
# Function commands with debug flag
npx supabase functions list --debug # List all edge functions with detailed output
npx supabase functions serve --debug # Serve functions locally with detailed logging
npx supabase functions deploy --debug # Deploy functions with detailed deployment logs- Clone the repository and install dependencies:
git clone https://github.com/HamChowderr/myMVP-starterkit.git cd myMVP-starterkit npm install
Create an empty .env.local file at the root of your project with one of these commands:
Windows (PowerShell):
New-Item -Path .env.local -ItemType "file" -ForceMac/Linux (Bash):
touch .env.localThen add the following content to the file:
# supabase
# These values never change when supabase is ran locally regardless of project
NEXT_PUBLIC_SUPABASE_URL=http://localhost:54321/
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU
SUPABASE_DATABASE_PASSWORD=postgres
SUPABASE_JWT_SECRET=super-secret-jwt-token-with-at-least-32-characters-long
# SUPABASE_PROJECT_REF=SUPABASE_PROJECT_REF
# stripe
STRIPE_SECRET_KEY=STRIPE_SECRET_KEY
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
STRIPE_WEBHOOK_SECRET=STRIPE_WEBHOOK_SECRET
# host
NEXT_PUBLIC_SITE_URL=http://localhost:3000
Note: You'll only need to update the STRIPE_WEBHOOK_SECRET for now. We'll configure the other Stripe keys later when setting up the MCP server.
-
Start Supabase with debug mode enabled:
npx supabase start --debug
This will start all the Supabase services using Docker. The first time you run this it will take a while to download the Docker images.
IMPORTANT: The current project ID name in
supabase/config.tomlcan be used once, but if you create multiple projects with this template, you must change theproject_idvalue in the file to a unique name for each new project to avoid Docker conflicts.IMPORTANT: Always remember to run
npx supabase stopbefore closing the project to properly shut down all Supabase services.
Your Supabase instance is now running on http://localhost:54321, and you can access the Supabase Studio on http://localhost:54323.
If you encounter an error like this:
failed to start docker container: Error response from daemon: failed to set up container networking: driver failed programming external connectivity on endpoint supabase_inbucket_starter-kit: failed to bind host port for 0.0.0.0:54324: address already in use
Try these solutions:
-
Stop all Supabase instances and remove containers:
npx supabase stop docker rm -f $(docker ps -a -q --filter "name=supabase") -
Check and kill processes using conflicting ports:
# Windows (PowerShell) netstat -ano | findstr 5432 # Then kill the process: taskkill /PID [PID] /F
-
Restart Docker Desktop completely
-
Change the project ID in
supabase/config.tomlto a unique name
- Copy the following SQL query:
-- Create products table with gateway-agnostic design
CREATE TABLE IF NOT EXISTS public.billing_products (
gateway_product_id TEXT PRIMARY KEY,
gateway_name TEXT NOT NULL,
name TEXT NOT NULL,
description TEXT,
features JSONB,
active BOOLEAN NOT NULL DEFAULT TRUE,
is_visible_in_ui BOOLEAN NOT NULL DEFAULT TRUE,
UNIQUE(gateway_name, gateway_product_id)
);
-- Create prices table with gateway-agnostic design
CREATE TABLE IF NOT EXISTS public.billing_prices (
gateway_price_id TEXT PRIMARY KEY DEFAULT gen_random_uuid(),
gateway_product_id TEXT NOT NULL REFERENCES public.billing_products(gateway_product_id) ON DELETE CASCADE,
currency TEXT NOT NULL,
amount DECIMAL NOT NULL,
recurring_interval TEXT NOT NULL,
recurring_interval_count INT NOT NULL DEFAULT 0,
active BOOLEAN NOT NULL DEFAULT TRUE,
tier TEXT,
free_trial_days INT,
gateway_name TEXT NOT NULL,
UNIQUE(gateway_name, gateway_price_id)
);
-- Add RLS
ALTER TABLE public.billing_products ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.billing_prices ENABLE ROW LEVEL SECURITY;
-- Create policies
CREATE POLICY "Allow public read-only access to billing_products" ON public.billing_products
FOR SELECT USING (true);
-- Add policy for service role to manage billing_products
CREATE POLICY "Allow service role to manage billing_products" ON public.billing_products
FOR ALL USING (auth.role() = 'service_role');
CREATE POLICY "Allow public read-only access to billing_prices" ON public.billing_prices
FOR SELECT USING (true);
-- Add policy for service role to manage billing_prices
CREATE POLICY "Allow service role to manage billing_prices" ON public.billing_prices
FOR ALL USING (auth.role() = 'service_role');
-- Create customers table with gateway-agnostic design
CREATE TABLE IF NOT EXISTS public.billing_customers (
gateway_customer_id TEXT PRIMARY KEY,
user_id UUID NOT NULL,
gateway_name TEXT NOT NULL,
default_currency TEXT,
billing_email TEXT NOT NULL,
metadata JSONB DEFAULT '{}',
UNIQUE (gateway_name, gateway_customer_id)
);
-- Enable RLS for customers
ALTER TABLE public.billing_customers ENABLE ROW LEVEL SECURITY;
-- Create policy for customers
CREATE POLICY "Allow workspace members to view their customers" ON public.billing_customers
FOR SELECT USING (true);
-- Add policy for service role to manage billing_customers
CREATE POLICY "Allow service role to manage billing_customers" ON public.billing_customers
FOR ALL USING (auth.role() = 'service_role');
-- Create invoices table with gateway-agnostic design
CREATE TABLE IF NOT EXISTS public.billing_invoices (
gateway_invoice_id TEXT PRIMARY KEY,
gateway_customer_id TEXT NOT NULL REFERENCES public.billing_customers(gateway_customer_id) ON DELETE CASCADE,
gateway_product_id TEXT REFERENCES public.billing_products(gateway_product_id) ON DELETE CASCADE,
gateway_price_id TEXT REFERENCES public.billing_prices(gateway_price_id) ON DELETE CASCADE,
gateway_name TEXT NOT NULL,
amount DECIMAL NOT NULL,
currency TEXT NOT NULL,
STATUS TEXT NOT NULL,
due_date DATE,
paid_date DATE,
hosted_invoice_url TEXT,
UNIQUE(gateway_name, gateway_invoice_id)
);
-- Enable RLS for invoices
ALTER TABLE public.billing_invoices ENABLE ROW LEVEL SECURITY;
-- Create policy for invoices
CREATE POLICY "Allow workspace members to view their invoices" ON public.billing_invoices
FOR SELECT USING (true);
-- Add policy for service role to manage billing_invoices
CREATE POLICY "Allow service role to manage billing_invoices" ON public.billing_invoices
FOR ALL USING (auth.role() = 'service_role');
-- Create one-time payments table with gateway-agnostic design
CREATE TABLE IF NOT EXISTS public.billing_one_time_payments (
gateway_charge_id TEXT PRIMARY KEY NOT NULL,
gateway_customer_id TEXT NOT NULL REFERENCES public.billing_customers(gateway_customer_id) ON DELETE CASCADE,
gateway_name TEXT NOT NULL,
amount DECIMAL NOT NULL,
currency TEXT NOT NULL,
STATUS TEXT NOT NULL,
charge_date TIMESTAMP WITH TIME ZONE NOT NULL,
gateway_invoice_id TEXT NOT NULL REFERENCES public.billing_invoices(gateway_invoice_id) ON DELETE CASCADE,
gateway_product_id TEXT NOT NULL REFERENCES public.billing_products(gateway_product_id) ON DELETE CASCADE,
gateway_price_id TEXT NOT NULL REFERENCES public.billing_prices(gateway_price_id) ON DELETE CASCADE
);
-- Enable RLS for one-time payments
ALTER TABLE public.billing_one_time_payments ENABLE ROW LEVEL SECURITY;
-- Create policy for one-time payments
CREATE POLICY "Allow workspace members to view their one-time payments" ON public.billing_one_time_payments
FOR SELECT USING (true);
-- Add policy for service role to manage billing_one_time_payments
CREATE POLICY "Allow service role to manage billing_one_time_payments" ON public.billing_one_time_payments
FOR ALL USING (auth.role() = 'service_role');
-- Create subscriptions table with gateway-agnostic design
CREATE TABLE IF NOT EXISTS public.billing_subscriptions (
id UUID PRIMARY KEY DEFAULT "extensions"."uuid_generate_v4"() NOT NULL,
gateway_customer_id TEXT NOT NULL REFERENCES public.billing_customers(gateway_customer_id) ON DELETE CASCADE,
gateway_name TEXT NOT NULL,
gateway_subscription_id TEXT NOT NULL,
gateway_product_id TEXT NOT NULL REFERENCES public.billing_products(gateway_product_id) ON DELETE CASCADE,
gateway_price_id TEXT NOT NULL REFERENCES public.billing_prices(gateway_price_id) ON DELETE CASCADE,
STATUS public.subscription_status NOT NULL,
current_period_start DATE NOT NULL,
current_period_end DATE NOT NULL,
currency TEXT NOT NULL,
is_trial BOOLEAN NOT NULL,
trial_ends_at DATE,
cancel_at_period_end BOOLEAN NOT NULL,
quantity INT,
UNIQUE(gateway_name, gateway_subscription_id)
);
-- Enable RLS for subscriptions
ALTER TABLE public.billing_subscriptions ENABLE ROW LEVEL SECURITY;
-- Create policy for subscriptions
CREATE POLICY "Allow workspace members to view their subscriptions" ON public.billing_subscriptions
FOR SELECT USING (true);
-- Add policy for service role to manage billing_subscriptions
CREATE POLICY "Allow service role to manage billing_subscriptions" ON public.billing_subscriptions
FOR ALL USING (auth.role() = 'service_role');
-- Example queries for your application:
-- Select all products
SELECT *, billing_prices(*)
FROM billing_products
WHERE gateway_name = 'stripe';
-- Select single product
SELECT *, billing_prices(*)
FROM billing_products
WHERE gateway_product_id = :productId
AND gateway_name = 'stripe'
LIMIT 1;
-- Upsert product
INSERT INTO billing_products (
gateway_product_id, gateway_name, name, description,
is_visible_in_ui, features, active
)
VALUES (
:productId, 'stripe', :name, :description,
:isVisibleInUi, :features, :active
)
ON CONFLICT (gateway_product_id, gateway_name)
DO UPDATE SET
name = :name,
description = :description,
is_visible_in_ui = :isVisibleInUi,
features = :features,
active = :active;
-- Update product visibility
UPDATE billing_products
SET is_visible_in_ui = :isVisible
WHERE gateway_product_id = :productId
AND gateway_name = 'stripe';
-- Select single price
SELECT *
FROM billing_prices
WHERE gateway_price_id = :priceId
AND gateway_name = 'stripe'
LIMIT 1;
-- Upsert price
INSERT INTO billing_prices (
gateway_product_id, gateway_name, gateway_price_id,
currency, amount, recurring_interval,
recurring_interval_count, active
)
VALUES (
:productId, 'stripe', :priceId,
:currency, :amount, :recurringInterval,
:recurringIntervalCount, :active
)
ON CONFLICT (gateway_price_id)
DO UPDATE SET
currency = :currency,
amount = :amount,
recurring_interval = :recurringInterval,
recurring_interval_count = :recurringIntervalCount,
active = :active;
-- Insert new customer
INSERT INTO billing_customers (
gateway_name, gateway_customer_id, billing_email, user_id
)
VALUES (
'stripe', :customerId, :billingEmail, auth.uid()
)
RETURNING *;
-- Get customer by customer ID
SELECT *
FROM billing_customers
WHERE gateway_customer_id = :customerId
AND gateway_name = 'stripe'
LIMIT 1;
-- Get customer by user ID
SELECT *
FROM billing_customers
WHERE user_id = auth.uid()
AND gateway_name = 'stripe'
LIMIT 1;
-- Get subscriptions by user ID
SELECT s.*
FROM billing_subscriptions s
JOIN billing_customers c ON s.gateway_customer_id = c.gateway_customer_id
WHERE c.user_id = auth.uid()
AND s.gateway_name = 'stripe';
-- Get invoices by user ID
SELECT i.*
FROM billing_invoices i
JOIN billing_customers c ON i.gateway_customer_id = c.gateway_customer_id
WHERE c.user_id = auth.uid()
AND i.gateway_name = 'stripe';
-- Get one-time purchases by customer ID
SELECT *, billing_products(*), billing_prices(*), billing_invoices(*)
FROM billing_one_time_payments
WHERE gateway_customer_id = :customerId
AND gateway_name = 'stripe';-
Paste it into the AI Assistant chat and say: "Create these tables for me in Supabase."
-
Run the migration to save these changes:
npx supabase migration new add_billing_tables
-
Apply the migration to your Supabase database:
npx supabase migration up
If you encounter any errors, try running with the debug flag:
npx supabase migration up --debug
-
If you continue to have issues, you may need to reset your local database:
npx supabase db reset
Note: This will erase all data in your local database and reapply migrations.
-
The AI will help create the tables in Supabase Studio, and the migration will preserve them in your project.
You can also create the tables manually:
- Access the Supabase Studio at http://localhost:54323
- Go to the SQL Editor
- Paste and run the SQL query above
-
Login to Stripe via CLI:
stripe login
This will prompt you to open a link in your browser. Complete the login process, then return to your terminal.
-
Forward Webhook Events to Your Local Server:
stripe listen --forward-to localhost:3000/api/stripe/webhooks
This will output a webhook signing secret (e.g.,
whsec_...). -
Add the Webhook Secret to Your Environment:
STRIPE_WEBHOOK_SECRET=whsec_...Update the
STRIPE_WEBHOOK_SECRETin your.env.localfile with this value and save the file for the changes to take effect.
Start your Next.js application:
npm run devYour application is now running on http://localhost:3000.
With everything set up, trigger the Stripe events to create products and prices:
stripe trigger product.created
stripe trigger price.createdYou should see a 200 response in your console for each successful webhook event, confirming that your application has processed the events correctly.
- Open http://localhost:54323 to access Supabase Studio
- Use the Table Editor to confirm that the products and prices have been created in your Supabase database
- Visit your application at http://localhost:3000 to verify it's functioning correctly
The Supabase Model Context Protocol allows you to interact with your Supabase database using AI assistants that support MCP. The Supabase MCP server automatically connects to your local Supabase instance, so no additional setup is required if you've already started Supabase locally.
Important: The Supabase MCP server is pre-configured to connect to your local Supabase instance. No configuration is needed as it works automatically after you run Supabase locally.
The project has been configured to automatically load the Supabase MCP server when using Cursor. The configuration is in .cursor/mcp.json.
Note: You may need to restart Cursor after starting Supabase locally for the MCP connection to work properly.
The Supabase MCP server provides read-only SQL access to your database, allowing you to:
- Query database tables and views using SELECT statements
- Verify data storage from webhook events
- Check database schema
- Test read operations
- Debug data-related issues
Important: The Supabase MCP tool is configured for read-only operations only. Do not attempt to use it for any data modification (INSERT, UPDATE, DELETE) or schema changes. For database modifications, always create proper migration files using the Supabase migration commands.
These tools can be accessed through AI assistants that support the Model Context Protocol.
The Stripe Model Context Protocol allows you to interact with Stripe API using AI assistants that support MCP.
Before using the Stripe MCP server, you need to configure your Stripe API key:
Edit the .cursor/mcp.json file to include your Stripe API key:
{
"mcpServers": {
"stripe": {
"command": "cmd",
"args": ["/c", "npx", "-y", "@stripe/mcp", "--tools=all", "--api-key=sk_test_YOUR_KEY_HERE"]
}
}
}Replace sk_test_YOUR_KEY_HERE with your actual Stripe secret key.
The project has been configured to automatically load the Stripe MCP server when using Cursor.
Note: You may need to restart Cursor after updating the API key in the configuration file for the changes to take effect.
Important: When you first start the application in Cursor, you will see a notification that "2 MCPs have been detected". You should press "Enable" to activate both the Stripe and Supabase MCPs.
The Stripe MCP server provides the following tools:
- Customers: Create and read customer information
- Products: Create and read product information
- Prices: Create and read price information
- Payment Links: Create payment links
- Invoices: Create and update invoices
- Balance: Retrieve balance information
- Refunds: Create refunds
- Payment Intents: Read payment intent information
- Subscriptions: Read and update subscription information
- Coupons: Create and read coupon information
- Documentation: Search Stripe documentation
These tools can be accessed through AI assistants that support the Model Context Protocol.
The VAPI Model Context Protocol allows you to interact with your VAPI assistants directly through Cursor. The MCP server is pre-configured in this project and just needs your API key to work.
To use the VAPI MCP, you only need to add your VAPI API key to the configuration file:
-
Edit the
.cursor/mcp.jsonfile to include your VAPI API key:"vapi-mcp-server": { "command": "npx", "args": [ "-y", "@vapi-ai/mcp-server" ], "env": { "VAPI_TOKEN": "your_vapi_token_here" } }
Replace
your_vapi_token_herewith your actual VAPI token. -
Restart Cursor to apply the changes.
The VAPI MCP server will automatically load when you restart Cursor, and you'll be able to manage your voice assistants directly through the MCP tools.
The VAPI MCP server provides tools to:
- List all your VAPI assistants
- Create new voice assistants
- Update existing assistants
- Get assistant details
- Manage calls and phone numbers
- Configure voice models and transcription services
These tools make it easy to create and manage voice assistants without leaving your development environment.
This project uses the Supabase UI Library and shadcn/ui blocks for rapid, type-safe, and beautiful UI development. You must run these commands once after cloning the repo to scaffold all required UI, authentication, and utility components.
npx shadcn@latest add https://supabase.com/ui/r/supabase-client-nextjs.json
- Sets up a Supabase client for SSR and App Router support with Next.js
- Provides authentication helpers, type-safe database queries, and server-side rendering support
- If you already have a Supabase client, you may skip this step.
npx shadcn@latest add https://supabase.com/ui/r/password-based-auth-nextjs.json
- Adds complete authentication flow with sign-in, sign-up, password reset, and account management
- Creates responsive, accessible, themed auth pages and components
- Implements security best practices and proper error handling
- See the environment variables section above for required environment variables.
npx shadcn@latest add https://supabase.com/ui/r/dropzone-nextjs.json
- Adds a complete drag-and-drop file upload system with preview and progress indicators
- Integrates directly with Supabase Storage for file handling
- Includes built-in validation, error handling, and accessibility features
npx shadcn@latest add https://supabase.com/ui/r/infinite-query-hook.json
- Implements efficient data pagination with infinite scrolling capabilities
- Provides optimized performance for large datasets with React Query integration
- Includes TypeScript types and simple API for querying Supabase tables