Skip to content

tomanagle/indiflow

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

46 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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 using @better-auth/stripe
  • 📧 Email System – Transactional emails with React Email and Resend
  • 🖼️ File Storage – Support for S3-compatible storage with pre-signed URLs
  • 🐞 Error Tracking – Integrated error reporting with Sentry
  • 🛠️ 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

  • Bun (recommended package manager)
  • PostgreSQL database (local or cloud)

Installation

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

Fill in the .env file with your own values:

VITE_BASE_URL=http://localhost:3000

DATABASE_URL=postgresql://username:password@localhost:5432/database
# You can also use Docker Compose to set up a local PostgreSQL database:
# docker-compose up -d

# Better Auth setup
BETTER_AUTH_SECRET=generate_a_random_string_here

# OAuth2 Providers (optional)
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=

# Stripe configuration (for subscription features)
STRIPE_WEBHOOK_SECRET=
STRIPE_SECRET_KEY=
VITE_STRIPE_PUBLISHABLE_KEY=

# Email configuration
RESEND_API_KEY=
FROM_EMAIL=Your SaaS <onboarding@yourdomain.com>

# Sentry error tracking (optional)
VITE_SENTRY_DSN=
SENTRY_AUTH_TOKEN=
SENTRY_ORG=
SENTRY_PROJECT=

# S3 Compatible Storage for Files/Media
S3_ACCESS_KEY=
S3_SECRET_KEY=
S3_REGION=us-east-1
S3_ENDPOINT=https://s3.amazonaws.com
S3_BUCKET_NAME=your-bucket-name
S3_PUBLIC_URL=https://your-bucket-name.s3.amazonaws.com

# Cloudflare R2
# S3_ACCESS_KEY=your_cloudflare_access_key_id
# S3_SECRET_KEY=your_cloudflare_secret_access_key
# S3_REGION=auto
# S3_ENDPOINT=https://<ACCOUNT_ID>.r2.cloudflarestorage.com
# S3_BUCKET_NAME=your_r2_bucket_name
# S3_PUBLIC_URL=https://<CUSTOM_DOMAIN>.com

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.

// Import in client-side code
import { clientEnv } from "../lib/client/env.client";

// Access variables
const stripeKey = clientEnv.VITE_STRIPE_PUBLISHABLE_KEY;
const baseUrl = clientEnv.VITE_BASE_URL;

Server-Side Environment Variables

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

// Import in server-side code only
import { serverEnv } from "../lib/server/env.server";

// Access variables
const stripeSecret = serverEnv.STRIPE_SECRET_KEY;
const databaseUrl = serverEnv.DATABASE_URL;

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:
bun dev

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:

bun db:push

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 description and marketing features in Stripe:
    • The Description field is displayed as a subtitle under the plan name on the pricing page
    • Marketing Features are displayed as bullet points with checkmarks on the pricing table
    • Both will automatically appear on your pricing page with no additional code required
  8. Add limits to your subscription plans by setting metadata fields with a limit: prefix. For example:
    • limit:users=100 - Sets a limit of 100 users for this plan
    • limit:storage=5120 - Sets a storage limit of 5GB (in MB) for this plan
    • limit:projects=10 - Sets a limit of 10 projects for this plan
  9. Activate the billing portal here: https://dashboard.stripe.com/test/settings/billing/portal

Setting Up Webhooks

  1. Go to Developers > Webhooks and add a new endpoint
  2. Set the endpoint to ${YOUR_DOMAIN}/api/auth/stripe/webhook
  3. Choose the following events:
   - customer.subscription.created
   - customer.subscription.updated
   - customer.subscription.deleted
   - price.updated
   - price.created
   - price.deleted
   - product.updated
   - product.created
   - 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:

stripe listen --forward-to localhost:3000/api/auth/stripe/webhook

Using Subscription Limits in Your Code

The system automatically reads any metadata fields with the limit: prefix from your Stripe products and makes them available in your application. These limits are also automatically displayed on your pricing table.

Limits on the Pricing Table

Limits you define in Stripe will automatically appear on the pricing page for each plan, displayed with a coin icon (🪙) to differentiate them from regular features. For example:

  • A limit set as limit:users=100 will appear as "100 x users"
  • A limit set as limit:storage=5120 will appear as "5120 x storage"
  • A limit set as limit:projects=10 will appear as "10 x projects"

The formatting is handled by the PricingTable component, which:

  1. Fetches all product data including limits from Stripe
  2. Renders marketing features with checkmarks (✓)
  3. Renders limits with coin icons (🪙)

No additional code is required to display these limits—they're automatically fetched from Stripe and displayed as part of the pricing table.

If you need to customize how limits are displayed, you can modify the PricingTable component in src/lib/components/PricingTable.tsx. Look for the following code block:

{Object.entries(product.limits).map(([key, value]) => (
  <li className="flex space-x-2" key={key}>
    <CoinsIcon className="mt-0.5 h-4 w-4 flex-shrink-0 text-yellow-500" />
    <span className="text-muted-foreground">
      {value} x {key}
    </span>
  </li>
))}

You can customize this to format specific limits differently, for example to show storage in GB instead of raw MB values.

Using Limits Programmatically

Here's how to use the limits in your application code:

// Server-side code to check subscription limits
import { useTRPC } from "@//trpc/react";
import { TRPCError } from "@trpc/server";

// In your tRPC procedure
const { ctx } = opts;
const { user, subscription } = ctx;

// Example: Check if user is within their plan's user limit
if (!subscription) {
  throw new TRPCError({
    code: "FORBIDDEN",
    message: "You need a subscription to perform this action",
  });
}

// Get the plan limit
const userLimit = subscription.limits.users || 0; 

// Query current usage
const currentUsersCount = await db.query.users.count({
  where: eq(users.organizationId, user.organizationId),
});

// Enforce the limit
if (currentUsersCount >= userLimit) {
  throw new TRPCError({
    code: "FORBIDDEN",
    message: "You've reached your plan's user limit. Please upgrade to add more users.",
  });
}

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 RESEND_API_KEY in your .env file
  3. Update the FROM_EMAIL in your .env file
  4. To test email templates locally:
bun email:dev

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


Sentry Error Tracking

This starter includes Sentry integration for error tracking and monitoring.

  1. Sign up for Sentry
  2. Create a new project
  3. Add the following environment variables to your .env file:
# Sentry configuration
VITE_SENTRY_DSN=your_sentry_dsn_url
SENTRY_AUTH_TOKEN=your_sentry_auth_token
SENTRY_ORG=your_sentry_organization
SENTRY_PROJECT=your_sentry_project_name

Where:

  • VITE_SENTRY_DSN is the client-side DSN URL from your Sentry project settings
  • SENTRY_AUTH_TOKEN is used for source map uploads (create in Account Settings > API)
  • SENTRY_ORG is your Sentry organization slug
  • SENTRY_PROJECT is your Sentry project slug

The integration is configured to:

  • Track errors on both client and server sides
  • Upload source maps automatically in production builds
  • Add Sentry middleware for handling server-side errors

You can access the error data in your Sentry dashboard and receive alerts when errors occur.


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:

bun 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

Vercel

Guide: https://tanstack.com/start/latest/docs/framework/react/hosting#vercel

  1. Create a Vercel account
  2. Connect your GitHub repository
  3. Add environment variables
  4. Uncomment the preset: vercel line in app.config.ts
  5. Deploy

Other Deployment Options


Development Commands

Start development server

bun dev

Build for production

bun build

Start production server

bun start

Lint code

bun lint

Format code

bun format

Generate UI components

bun ui

Manage database schema

bun db:push

Generate auth schema

bun auth:generate

Start email development server

bun 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:
S3_ACCESS_KEY=your_cloudflare_access_key_id
S3_SECRET_KEY=your_cloudflare_secret_access_key
S3_REGION=auto
S3_ENDPOINT=https://<ACCOUNT_ID>.r2.cloudflarestorage.com
S3_BUCKET_NAME=your_r2_bucket_name
S3_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:
[
  {
    "AllowedOrigins": ["https://yourdomain.com", "http://localhost:3000"],
    "AllowedMethods": ["GET", "PUT", "POST"],
    "AllowedHeaders": ["*"],
    "ExposeHeaders": [],
    "MaxAgeSeconds": 3000
  }
]

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

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages