···2233your data, your space, use it enywhere.
4455-A full-stack TypeScript application using Next.js for processing hosting service purchases.
55+A full-stack TypeScript application using Next.js with Supabase Auth and Stripe subscriptions for access-controlled hosting services.
6677## Features
8899-- **Checkout** - Custom amount hosting service purchases with hosted checkout
1010-- **Payment Elements** - Custom payment form with Payment Element
1111-- **Webhook handling** - Server-side webhook processing for payment events
99+- **Authentication** - Email-based authentication with Supabase Auth
1010+- **Subscriptions** - Stripe subscription checkout and management
1111+- **Dashboard** - User dashboard showing subscription status
1212+- **Protected API** - Server endpoints only accessible to subscribed users
1313+- **Webhook handling** - Server-side webhook processing for subscription events
12141315## Tech Stack
14161517- **Frontend**: Next.js, React, TypeScript
1618- **Backend**: Next.js Server Actions and Route Handlers
1919+- **Auth**: Supabase Auth
2020+- **Database**: Supabase PostgreSQL
2121+- **Payments**: Stripe Subscriptions
17221823## Getting Started
19242025### Prerequisites
21262227- Node.js 18+ installed
2323-- A payment processor account
2828+- A Supabase account and project
2929+- A Stripe account
24302531### Installation
2632···3440pnpm install
3541```
36423737-2. Set up environment variables:
4343+2. Set up Supabase:
4444+4545+- Create a new Supabase project at [supabase.com](https://supabase.com)
4646+- Run the migration file to create the subscriptions table:
4747+ - Go to your Supabase project dashboard
4848+ - Navigate to SQL Editor
4949+ - Copy and run the contents of `supabase/migrations/001_subscriptions.sql`
5050+5151+3. Set up environment variables:
38523953Create a `.env.local` file in the root directory:
40544155```bash
5656+# Supabase
5757+NEXT_PUBLIC_SUPABASE_URL=your_supabase_project_url
5858+NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
5959+SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key
6060+6161+# Stripe
4262NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=your_publishable_key
4363STRIPE_SECRET_KEY=your_secret_key
4464STRIPE_WEBHOOK_SECRET=your_webhook_secret
6565+NEXT_PUBLIC_STRIPE_PRICE_ID=your_stripe_price_id
6666+6767+# App URL (for redirects)
6868+NEXT_PUBLIC_APP_URL=http://localhost:3000
4569```
46704747-Get your API keys from your payment processor dashboard.
7171+Get your Supabase keys from your project settings → API.
7272+Get your Stripe keys from your Stripe dashboard.
7373+Create a subscription product and price in Stripe, then use the price ID for `NEXT_PUBLIC_STRIPE_PRICE_ID`.
487449753. Start the development server:
5076···62886389#### Local Development
64906565-1. Install the payment processor CLI and link your account.
9191+1. Install the Stripe CLI and link your account:
9292+9393+```bash
9494+stripe login
9595+```
669667972. Start webhook forwarding to your local server:
68986999```bash
7070-# Example command - adjust based on your payment processor
7171-webhook listen --forward-to localhost:3000/api/webhooks
100100+stripe listen --forward-to localhost:3000/api/webhooks
72101```
731027474-3. Copy the webhook secret from the CLI output and add it to your `.env.local` file.
103103+3. Copy the webhook signing secret from the CLI output and add it to your `.env.local` file as `STRIPE_WEBHOOK_SECRET`.
7510476105#### Production
77106781071. Deploy your application and copy the webhook URL (e.g., `https://your-domain.com/api/webhooks`).
791088080-2. Create a webhook endpoint in your payment processor dashboard.
109109+2. In your Stripe dashboard, go to Developers → Webhooks and add an endpoint:
110110+ - URL: `https://your-domain.com/api/webhooks`
111111+ - Events to listen to:
112112+ - `checkout.session.completed`
113113+ - `customer.subscription.created`
114114+ - `customer.subscription.updated`
115115+ - `customer.subscription.deleted`
116116+ - `invoice.payment_succeeded`
117117+ - `invoice.payment_failed`
811188282-3. Add the webhook signing secret to your production environment variables.
119119+3. Copy the webhook signing secret and add it to your production environment variables as `STRIPE_WEBHOOK_SECRET`.
8312084121## Testing
85122···97134## Project Structure
9813599136- `app/` - Next.js app directory with pages and components
100100-- `app/actions/` - Server actions for payment operations
101101-- `app/api/webhooks/` - Webhook handler route
102102-- `lib/` - Payment processor client configuration
103103-- `components/` - React components for payment forms
137137+ - `dashboard/` - User dashboard with subscription status and protected actions
138138+ - `login/` - Login page
139139+ - `signup/` - Sign up page
140140+ - `actions/` - Server actions for auth and subscriptions
141141+ - `api/` - API routes (webhooks, protected server endpoints)
142142+- `lib/` - Client configurations (Stripe, Supabase)
143143+- `supabase/migrations/` - Database migrations
144144+- `components/` - React components
104145- `utils/` - Utility functions
146146+147147+## How It Works
148148+149149+1. **Authentication**: Users sign up/login with email via Supabase Auth
150150+2. **Subscription**: Users can subscribe via Stripe Checkout
151151+3. **Webhook Sync**: Stripe webhooks update subscription status in Supabase database
152152+4. **Access Control**: Dashboard shows subscription status and protected API buttons
153153+5. **Protected Routes**: `/api/server/[endpoint]` routes check for active subscription before allowing access
105154106155## Multi-Remote Git Setup
107156
···11+-- Create subscriptions table to track user subscriptions
22+CREATE TABLE IF NOT EXISTS subscriptions (
33+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
44+ user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
55+ stripe_customer_id TEXT,
66+ stripe_subscription_id TEXT UNIQUE,
77+ stripe_price_id TEXT,
88+ status TEXT NOT NULL,
99+ current_period_start TIMESTAMPTZ,
1010+ current_period_end TIMESTAMPTZ,
1111+ cancel_at_period_end BOOLEAN DEFAULT false,
1212+ created_at TIMESTAMPTZ DEFAULT NOW(),
1313+ updated_at TIMESTAMPTZ DEFAULT NOW()
1414+);
1515+1616+-- Create index for faster lookups
1717+CREATE INDEX IF NOT EXISTS subscriptions_user_id_idx ON subscriptions(user_id);
1818+CREATE INDEX IF NOT EXISTS subscriptions_stripe_customer_id_idx ON subscriptions(stripe_customer_id);
1919+CREATE INDEX IF NOT EXISTS subscriptions_stripe_subscription_id_idx ON subscriptions(stripe_subscription_id);
2020+2121+-- Enable Row Level Security
2222+ALTER TABLE subscriptions ENABLE ROW LEVEL SECURITY;
2323+2424+-- Policy: Users can only see their own subscriptions
2525+DROP POLICY IF EXISTS "Users can view own subscriptions" ON subscriptions;
2626+CREATE POLICY "Users can view own subscriptions"
2727+ ON subscriptions
2828+ FOR SELECT
2929+ USING (auth.uid() = user_id);
3030+3131+-- Policy: Users can insert their own subscription records (for initial customer creation)
3232+-- But they can ONLY insert records with status='incomplete' to prevent fraud
3333+DROP POLICY IF EXISTS "Users can insert own subscriptions" ON subscriptions;
3434+CREATE POLICY "Users can insert own subscriptions"
3535+ ON subscriptions
3636+ FOR INSERT
3737+ WITH CHECK (
3838+ auth.uid() = user_id
3939+ AND status = 'incomplete'
4040+ );
4141+4242+-- Users CANNOT update their own subscriptions directly
4343+-- All updates must come from webhooks (using service role) or validated server actions
4444+-- This prevents users from setting status='active' without paying
4545+DROP POLICY IF EXISTS "Users can update own subscriptions" ON subscriptions;
4646+4747+-- Note: Service role (used by webhooks via admin client) bypasses RLS entirely
4848+-- The admin client uses SUPABASE_SERVICE_ROLE_KEY which automatically bypasses all RLS policies.
4949+-- No policy needed for service role since it bypasses RLS.
5050+5151+-- Function to update updated_at timestamp
5252+CREATE OR REPLACE FUNCTION update_updated_at_column()
5353+RETURNS TRIGGER AS $$
5454+BEGIN
5555+ NEW.updated_at = NOW();
5656+ RETURN NEW;
5757+END;
5858+$$ language 'plpgsql';
5959+6060+-- Trigger to automatically update updated_at
6161+DROP TRIGGER IF EXISTS update_subscriptions_updated_at ON subscriptions;
6262+CREATE TRIGGER update_subscriptions_updated_at
6363+ BEFORE UPDATE ON subscriptions
6464+ FOR EACH ROW
6565+ EXECUTE FUNCTION update_updated_at_column();
···11+-- Add policies to allow users to insert and update their own subscriptions
22+-- This is needed for the checkout flow to work without requiring service role key
33+-- NOTE: This migration is now obsolete - we removed UPDATE policy for security
44+-- Keeping it for migration history, but it will be skipped if policies already exist
55+66+-- Policy: Users can insert their own subscription records (for initial customer creation)
77+DO $$
88+BEGIN
99+ IF NOT EXISTS (
1010+ SELECT 1 FROM pg_policies
1111+ WHERE schemaname = 'public'
1212+ AND tablename = 'subscriptions'
1313+ AND policyname = 'Users can insert own subscriptions'
1414+ ) THEN
1515+ CREATE POLICY "Users can insert own subscriptions"
1616+ ON subscriptions
1717+ FOR INSERT
1818+ WITH CHECK (auth.uid() = user_id);
1919+ END IF;
2020+END $$;
···11+-- Remove the UPDATE policy that allows users to update their own subscriptions
22+-- This is a security fix: users should NOT be able to modify subscription status
33+-- All updates must come from webhooks (service role) or validated server actions
44+55+-- This migration is safe to run multiple times
66+DO $$
77+BEGIN
88+ IF EXISTS (
99+ SELECT 1 FROM pg_policies
1010+ WHERE schemaname = 'public'
1111+ AND tablename = 'subscriptions'
1212+ AND policyname = 'Users can update own subscriptions'
1313+ ) THEN
1414+ DROP POLICY "Users can update own subscriptions" ON subscriptions;
1515+ END IF;
1616+END $$;
···11+-- Additional security: Add a check constraint to ensure data integrity
22+-- Note: RLS policies already prevent users from updating, but this adds an extra layer
33+44+-- Ensure status is one of the valid Stripe subscription statuses
55+ALTER TABLE subscriptions
66+ DROP CONSTRAINT IF EXISTS valid_subscription_status;
77+88+ALTER TABLE subscriptions
99+ ADD CONSTRAINT valid_subscription_status
1010+ CHECK (status IN (
1111+ 'incomplete',
1212+ 'incomplete_expired',
1313+ 'trialing',
1414+ 'active',
1515+ 'past_due',
1616+ 'canceled',
1717+ 'unpaid',
1818+ 'paused'
1919+ ));