A full-stack invoice management platform built with Next.js 15, Drizzle ORM, Stripe Connect, and Resend. Create and send professional invoices, accept payments online, and automatically deliver receipts to clients.
- Features
- Tech Stack
- Getting Started
- Usage
- Stripe Integration
- Deployment
- Project Structure
- Database Schema
- License
- 🔐 Google OAuth — Secure authentication via NextAuth.js
- 🧾 Invoice Management — Create, send, and track invoices with line items, due dates, and status tracking
- 👥 Client Management — Manage your client list with names and contact details
- 💳 Stripe Connect — Accept payments on behalf of users via Stripe Connect (platform model)
- 📄 PDF Generation — Auto-generate professional invoice and receipt PDFs using
@react-pdf/renderer - 📧 Email Delivery — Send invoices and payment receipts via Resend
- 🔔 Webhook Automation — Automatically mark invoices as paid and send receipts when payment completes
- 📊 Dashboard Stats — Revenue overview and invoice status breakdown
- 🔍 Filterable Table — Sort, filter, and paginate invoices with TanStack Table
- 📱 Responsive — Fully responsive layout built with Tailwind CSS and shadcn/ui
| Category | Technology |
|---|---|
| Framework | Next.js 15 (App Router) |
| Language | TypeScript |
| Database | PostgreSQL (via Neon) |
| ORM | Drizzle ORM |
| Auth | NextAuth.js v5 (Google OAuth) |
| Payments | Stripe Connect |
| Resend | |
| @react-pdf/renderer | |
| UI | shadcn/ui + Tailwind CSS |
| Tables | TanStack Table v8 |
| Deployment | Vercel |
Make sure you have the following installed:
- Node.js v18 or higher
- pnpm v8 or higher
- A Neon or any PostgreSQL database
- A Stripe account
- A Resend account
- A Google Cloud project for OAuth
# Clone the repository
git clone https://github.com/yourusername/invoice-saas.git
cd invoice-saas
# Install dependencies
pnpm install
# Copy environment variables
cp .env.example .env.localCreate a .env.local file in the root of the project and fill in the following values:
# App
NEXT_PUBLIC_BASE_URL=http://localhost:3000
# Database
DATABASE_URL=postgresql://...
# Auth
AUTH_SECRET=your-nextauth-secret
AUTH_GOOGLE_ID=your-google-client-id
AUTH_GOOGLE_SECRET=your-google-client-secret
# Stripe
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
# Resend
RESEND_API_KEY=re_...
RESEND_FROM_EMAIL=noreply@yourdomain.com
# Platform
PLATFORM_NAME=Your Company Name
PLATFORM_EMAIL=support@yourdomain.comNote: Never commit your
.env.localfile. It is already listed in.gitignore.
This project uses Drizzle ORM. Run the following commands to generate and apply migrations:
# Generate migration files from schema
pnpm drizzle-kit generate
# Apply migrations to your database
pnpm drizzle-kit migrateTo inspect your database visually:
pnpm drizzle-kit studio- Navigate to Invoices in the sidebar
- Click New Invoice
- Select a client, add line items, set a due date and payment terms
- The live preview on the right updates as you type
- Click Create Invoice — the invoice is saved with
draftstatus
- On the Invoices page, find the invoice you want to send
- Click the ⋯ actions menu on the right
- Select Send Email
- The client receives a professional email with the invoice PDF attached and a Pay Now link
- The invoice status automatically updates to
pending
Clients receive a unique payment link (/pay/[invoiceId]). Clicking Pay Now takes them to a Stripe-hosted checkout page. After payment:
- The invoice is automatically marked as
paid - The client receives a receipt email with a PDF containing full proof of payment
This app uses Stripe Connect (Standard accounts) so that each user can accept payments directly into their own Stripe account.
- Go to your Stripe Dashboard → Settings → Connect
- Enable Connect and note your platform client ID
- In the app, go to Settings and click Connect Stripe Account
- Complete the Stripe onboarding flow
- Once connected, your
stripeAccountIdis saved and clients can pay you directly
Webhooks allow Stripe to notify your server when a payment completes.
For local development, use the Stripe CLI:
# Install Stripe CLI
# Windows: https://github.com/stripe/stripe-cli/releases
# Mac: brew install stripe/stripe-cli/stripe
# Login
stripe login
# Forward webhooks to your local server
stripe listen --forward-to localhost:3000/api/stripe/webhookCopy the webhook signing secret printed in the terminal and add it to .env.local:
STRIPE_WEBHOOK_SECRET=whsec_... # ← the one from stripe listenFor production (Vercel):
- Go to Stripe Dashboard → Developers → Webhooks
- Click Add destination
- Select Your account (not Connected accounts)
- Select event:
checkout.session.completed - Choose Webhook as destination type
- Set the endpoint URL to:
https://yourdomain.vercel.app/api/stripe/webhook - Click Reveal next to Signing secret → copy the
whsec_...value - Add it to Vercel environment variables as
STRIPE_WEBHOOK_SECRET - Redeploy your Vercel project
Important: The local CLI secret and the Dashboard secret are different. Never mix them up.
This app is designed to deploy on Vercel.
# Install Vercel CLI
pnpm i -g vercel
# Deploy
vercelOr connect your GitHub repository to Vercel for automatic deployments on every push.
Required environment variables on Vercel (add under Project → Settings → Environment Variables):
NEXT_PUBLIC_BASE_URL
DATABASE_URL
AUTH_SECRET
AUTH_GOOGLE_ID
AUTH_GOOGLE_SECRET
STRIPE_SECRET_KEY
STRIPE_WEBHOOK_SECRET
RESEND_API_KEY
RESEND_FROM_EMAIL
PLATFORM_NAME
PLATFORM_EMAIL
After adding or changing environment variables, you must redeploy for changes to take effect.
invoice-saas/
├── app/
│ ├── (auth)/
│ │ └── login/ # Login page
│ ├── (dashboard)/
│ │ ├── page.tsx # Dashboard home + stats
│ │ ├── clients/ # Client list page
│ │ └── invoices/
│ │ ├── page.tsx # Invoices table
│ │ └── new/ # Create invoice form + live preview
│ ├── api/
│ │ ├── auth/ # NextAuth handlers
│ │ ├── client/ # Client CRUD
│ │ ├── invoices/ # Invoice CRUD + send + PDF
│ │ └── stripe/
│ │ ├── checkout/ # Create Stripe checkout session
│ │ ├── connect/ # Stripe Connect onboarding
│ │ └── webhook/ # Stripe webhook handler
│ ├── pay/[id]/ # Public client payment page
│ └── success/ # Post-payment success page
├── components/
│ ├── ui/ # shadcn/ui components
│ ├── app-sidebar.tsx # Main sidebar with user dropdown
│ ├── columns.tsx # TanStack Table column definitions
│ ├── data-table.tsx # Filterable, sortable invoice table
│ └── invoice-preview.tsx # Live invoice preview component
├── db/
│ ├── index.ts # Drizzle DB client
│ └── schema.ts # Database schema + types
├── drizzle/ # Auto-generated migration files
├── lib/
│ ├── auth.ts # NextAuth configuration
│ ├── stripe.ts # Stripe client
│ ├── email.ts # Invoice email sender (Resend)
│ ├── receipt-email.ts # Receipt email sender (Resend)
│ ├── pdf.tsx # Invoice PDF generator
│ ├── receipt-pdf.tsx # Receipt PDF generator
│ └── utils/
│ ├── zodSchema.ts # Zod validation schemas
│ └── utilityFunctions.ts # Helper functions
└── public/
└── logo/ # App logo assets
users
├── id uuid PK
├── email text unique
├── password text
├── stripeAccountId text
└── createdAt timestamp
clients
├── id uuid PK
├── userId uuid FK → users.id (cascade)
├── name text
├── email text
└── createdAt timestamp
invoices
├── id uuid PK
├── userId uuid FK → users.id (cascade)
├── clientId uuid FK → clients.id (cascade)
├── description text
├── items jsonb (InvoiceItem[])
├── amount_cents integer
├── dueDate timestamp
├── status enum (draft | pending | paid | overdue)
├── createdAt timestamp
└── updatedAt timestamp
InvoiceItem shape (stored in items JSONB column):
{
description: string;
quantity: number;
unitPrice: number; // in dollars (e.g. 50.00)
}MIT License — feel free to use this project as a portfolio piece or starting point for your own SaaS.
Built by Sameed Khan









