SaaS Starter Kit

A complete and production-ready starter kit for building a subscription-based SaaS platform.

Features

  • Authentication – Email/password and OAuth providers (GitHub, Google) via Better Auth
  • 💰 Subscription System – Complete Stripe integration with webhooks
  • 📧 Email System – Transactional emails with React Email and Resend
  • 🖼️ File Storage – Support for S3-compatible storage with pre-signed URLs
  • 🛠️ Full-Stack TypeScript – End-to-end type safety
  • 🎨 Modern UI Components – Built with Tailwind CSS and Radix UI
  • 🔄 State ManagementTanstack Query and tRPC for type-safe API calls
  • 🔍 Form HandlingTanstack Form with Zod validation
  • 🌓 Dark Mode – Built-in dark/light theme support
  • 📱 Responsive Design – Mobile-first approach
  • 🚀 Easy Deployment – Deploy to Railway, Vercel, or any other platform

Tech Stack

Getting Started

Prerequisites

  • Node.js (v22 or higher)
  • pnpm (recommended package manager)
  • PostgreSQL database (local or cloud)

Installation

  1. Clone the repository:
bash
1git clone https://github.com/tomangale/indiflow.git 2cd indiflow 3
  1. Install dependencies:
bash
1pnpm install 2
  1. Set up environment variables:
bash
1cp .env.example .env 2

Fill in the .env file with your own values:

bash
1VITE_BASE_URL=http://localhost:3000 2 3DATABASE_URL=postgresql://username:password@localhost:5432/database 4# You can also use Docker Compose to set up a local PostgreSQL database: 5# docker-compose up -d 6 7# Better Auth setup 8BETTER_AUTH_SECRET=generate_a_random_string_here 9 10# OAuth2 Providers (optional) 11GITHUB_CLIENT_ID= 12GITHUB_CLIENT_SECRET= 13GOOGLE_CLIENT_ID= 14GOOGLE_CLIENT_SECRET= 15 16# Stripe configuration (for subscription features) 17STRIPE_WEBHOOK_SECRET= 18STRIPE_SECRET_KEY= 19VITE_STRIPE_PUBLISHABLE_KEY= 20 21# Email configuration 22RESEND_API_KEY= 23FROM_EMAIL=Your SaaS <onboarding@yourdomain.com> 24 25# S3 Compatible Storage for Files/Media 26S3_ACCESS_KEY= 27S3_SECRET_KEY= 28S3_REGION=us-east-1 29S3_ENDPOINT=https://s3.amazonaws.com 30S3_BUCKET_NAME=your-bucket-name 31S3_PUBLIC_URL=https://your-bucket-name.s3.amazonaws.com 32 33# Cloudflare R2 34# S3_ACCESS_KEY=your_cloudflare_access_key_id 35# S3_SECRET_KEY=your_cloudflare_secret_access_key 36# S3_REGION=auto 37# S3_ENDPOINT=https://<ACCOUNT_ID>.r2.cloudflarestorage.com 38# S3_BUCKET_NAME=your_r2_bucket_name 39# S3_PUBLIC_URL=https://<CUSTOM_DOMAIN>.com 40

Environment variables usage

This starter uses a type-safe approach to environment variables with Zod validation:

Client-Side Environment Variables

All client-side environment variables must start with VITE_ to be accessible in the browser.

typescript
1// Import in client-side code 2import { clientEnv } from "../lib/client/env.client"; 3 4// Access variables 5const stripeKey = clientEnv.VITE_STRIPE_PUBLISHABLE_KEY; 6const baseUrl = clientEnv.VITE_BASE_URL; 7

Server-Side Environment Variables

Server-side variables are for sensitive data that should never be exposed to the client.

typescript
1// Import in server-side code only 2import { serverEnv } from "../lib/server/env.server"; 3 4// Access variables 5const stripeSecret = serverEnv.STRIPE_SECRET_KEY; 6const databaseUrl = serverEnv.DATABASE_URL; 7

The environment system ensures:

  • Type safety with Zod validation
  • Proper error messages for missing variables
  • Security through separation of client/server environments
  • Protection against accidentally exposing secrets to the client
  1. Start the development server:
bash
1pnpm dev 2

Visit http://localhost:3000 to see your application.

Database Setup

This starter uses Drizzle ORM with PostgreSQL. You can set up a local PostgreSQL instance or use a cloud provider like Neon or Supabase.

To push schema changes to your database:

bash
1pnpm db:push 2

Setting Up Stripe

Creating Products and Prices

  1. Register for a Stripe account
  2. On the dashboard, find and copy the Publishable key and add it to VITE_STRIPE_PUBLISHABLE_KEY in your .env file
  3. Find the Secret key and add it to STRIPE_SECRET_KEY in your .env file
  4. In test mode, go to Product catalog and create a new product
  5. Choose a name for the product and make sure the price is set to recurring
  6. Create a price for the monthly subscription and a price for the yearly subscription
  7. Fill in the product marketing features and description (these will automatically be added to the landing page)

Setting Up Webhooks

  1. Go to Developers > Webhooks and add a new endpoint
  2. Set the endpoint to ${YOUR_DOMAIN}/api/webhooks/stripe
  3. Choose the following events:
bash
1 - customer.subscription.created 2 - customer.subscription.updated 3 - customer.subscription.deleted 4 - price.updated 5 - price.created 6 - price.deleted 7 - product.updated 8 - product.created 9 - product.deleted
  1. Get the signing secret from the webhook settings and add it to STRIPE_WEBHOOK_SECRET in your .env file

For local development, you can use the Stripe CLI to forward webhooks to your local server:

bash
1stripe listen --forward-to localhost:3000/api/webhooks/stripe

Email Setup

This starter uses React Email with Resend for sending transactional emails.

  1. Sign up for Resend
  2. Create an API key and add it to RESENDAPIKEY in your .env file
  3. Update the FROM_EMAIL in your .env file
  4. To test email templates locally:
bash
1pnpm email:dev 2

This will start a local server at http://localhost:3030 where you can preview email templates.

Authentication

This starter uses Better Auth for authentication. It supports email/password authentication and OAuth providers.

To set up OAuth providers:

  1. Create OAuth apps with the providers you want to support (e.g., GitHub, Google)
  2. Add the client IDs and secrets to your .env file
  3. Set the callback URLs to http://localhost:3000/api/auth/callback/ (e.g., http://localhost:3000/api/auth/callback/github)

To generate new authentication schema:

bash
1pnpm auth:generate

Deployment

This starter can be deployed to any platform that supports Node.js. Here are some recommended options:

Railway (Recommended)

  1. Create a Railway account
  2. Create a new project
  3. Connect your GitHub repository
  4. Add environment variables
  5. Deploy

Other Deployment Options

Development Commands

Start development server

bash
1pnpm dev 2

Build for production

bash
1pnpm build 2

Start production server

bash
1pnpm start

Lint code

bash
1pnpm lint

Format code

bash
1pnpm format

Generate UI components

bash
1pnpm ui

Manage database schema

bash
1pnpm db:push

Generate auth schema

bash
1pnpm auth:generate 2

Start email development server

bash
1pnpm email:dev

Project Structure

  • /src - Source code
    • /lib - Core functionality
      • /client - Client-side utilities
        • env.client.ts - Type-safe client environment variables
      • /components - React components
      • /middleware - Application middleware
      • /server - Server-side code
        • env.server.ts - Type-safe server environment variables
        • /modules - Feature modules (user, stripe, email)
        • /schema - Database schema
        • /utils - Server utilities (logger, caching, etc.)
    • /routes - Application routes
      • __root.tsx - Root layout
      • _auth/ - Authentication routes (sign in, sign up)
      • api/ - API endpoints
      • console/ - User dashboard
      • profile/ - User profile management
      • subscription/ - Subscription management
    • /trpc - tRPC configuration
  • /emails - Email templates
  • /public - Static assets

File uploads

This starter includes support for file uploads to S3-compatible storage services, useful for user avatars and other media:

  1. Set up an S3 bucket (AWS S3, DigitalOcean Spaces, MinIO, etc.)
  2. Configure CORS on your bucket to allow uploads from your domain
  3. Add the following environment variables to your .env file: S3_ACCESS_KEY=your_access_key S3_SECRET_KEY=your_secret_key S3_REGION=your_region (e.g., us-east-1) S3_ENDPOINT=your_endpoint (e.g., https://s3.amazonaws.com) S3_BUCKET_NAME=your_bucket_name S3_PUBLIC_URL=your_bucket_public_url (e.g., https://your-bucket.s3.amazonaws.com)

For non-AWS S3 services, you may need to adjust:

  • S3_ENDPOINT to your provider's endpoint (e.g., https://nyc3.digitaloceanspaces.com for DigitalOcean)
  • S3_PUBLIC_URL to your provider's public URL format

The system uses pre-signed URLs for secure direct browser-to-S3 uploads, removing the load from your server.

Cloudflare R2

To use the media service with Cloudflare R2, you'll need to configure your credentials in the following way:

Get Cloudflare R2 Credentials:

  • Log into your Cloudflare dashboard
  • Navigate to R2 section
  • Create an R2 bucket if you haven't already
  • Generate API tokens with appropriate permissions (R2 Admin)
  • You'll get an Access Key ID and Secret Access Key

Environment Variables Setup:

  • Add these to your .env file:
bash
1S3_ACCESS_KEY=your_cloudflare_access_key_id 2S3_SECRET_KEY=your_cloudflare_secret_access_key 3S3_REGION=auto 4S3_ENDPOINT=https://<ACCOUNT_ID>.r2.cloudflarestorage.com 5S3_BUCKET_NAME=your_r2_bucket_name 6S3_PUBLIC_URL=https://<CUSTOM_DOMAIN>.com

Where:

  • <ACCOUNT_ID> is your Cloudflare account ID (found in dashboard URL)
  • <CUSTOM_DOMAIN> is your public domain for the bucket (optional)

CORS Configuration:

  • Configure CORS rules on your R2 bucket to allow uploads from your domain:
json
1[ 2 { 3 "AllowedOrigins": [ 4 "https://yourdomain.com", 5 "http://localhost:3000" 6 ], 7 "AllowedMethods": [ 8 "GET", 9 "PUT", 10 "POST" 11 ], 12 "AllowedHeaders": [ 13 "*" 14 ], 15 "ExposeHeaders": [], 16 "MaxAgeSeconds": 3000 17 } 18]

Public Access (Optional): If you want files to be publicly accessible, you'll need to:

  • Set up a Custom Domain for your R2 bucket in Cloudflare
  • Update S3PUBLICURL to point to this custom domain
  • Configure public access policies for your bucket

The media service will work with R2 without code changes because Cloudflare R2 is designed to be S3-compatible with the AWS SDK. The key difference is just the endpoint URL format.