Features
Invoices
Full invoice lifecycle management — from draft to paid. Generate Stripe payment links, sync to Google Sheets, and record payments with a single click.
Invoice Lifecycle
Every invoice moves through four statuses. The system automatically marks invoices as overdue when the due date passes.
Draft
The invoice is being prepared. Line items can be added, edited, or removed. Not visible to the client yet.
Sent
Delivered to the client via email or payment link. The due date countdown begins.
Paid
Payment received and recorded. The paid date is stored and the lead status can advance to Paid.
Overdue
The due date has passed without payment. Triggers a notification alert on the dashboard.
Draft → Sent → Paid
Sent invoices automatically become Overdue when past the due date
Stripe Payment Links
When you mark an invoice as "Sent", the app can generate a Stripe Checkout session so your client can pay online. The payment link is stored on the invoice record and can be shared via email.
- *Requires
STRIPE_SECRET_KEYin your environment - *Checkout sessions use the invoice amount and number as the product name
- *Success URL redirects back to the invoice page with a
?paid=trueparameter - *Webhook support for automatic payment confirmation is optional
// Generate a Stripe payment link for an invoice
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function generatePaymentLink(invoice: Invoice) {
const session = await stripe.checkout.sessions.create({
mode: "payment",
line_items: [{
price_data: {
currency: "usd",
product_data: { name: `Invoice #${invoice.number}` },
unit_amount: Math.round(invoice.amount * 100),
},
quantity: 1,
}],
success_url: `${process.env.AUTH_URL}/invoices/${invoice.id}?paid=true`,
cancel_url: `${process.env.AUTH_URL}/invoices/${invoice.id}`,
});
return session.url;
}Google Sheets Integration
For freelancers who track finances in spreadsheets, the CRM can sync invoice data to a Google Sheet. Each invoice creates or updates a row with the invoice number, client, amount, status, and dates.
- *Configure
GOOGLE_SHEETS_IDin your environment - *Rows are appended when invoices are created, updated when status changes
- *Uses the Google Sheets API v4 — requires a service account
// Sync invoice to Google Sheets
export async function syncToSheets(invoice: Invoice) {
// Appends a row to your configured Google Sheet
// Columns: Invoice #, Client, Amount, Status, Due Date, Paid Date
await sheets.spreadsheets.values.append({
spreadsheetId: process.env.GOOGLE_SHEETS_ID,
range: "Invoices!A:F",
valueInputOption: "USER_ENTERED",
requestBody: {
values: [[
invoice.number,
invoice.client.name,
invoice.amount,
invoice.status,
invoice.dueDate,
invoice.paidAt ?? "",
]],
},
});
}Recording Payments
Payments can be recorded in two ways:
- 1.Manual— Click "Mark as Paid" on the invoice detail page. Optionally enter the payment date and reference number.
- 2.Automatic via Stripe — When a client completes the Stripe checkout, the success redirect triggers an automatic status update to Paid.
Data Model
// Invoice model and status
enum InvoiceStatus {
DRAFT // Being prepared, not yet sent
SENT // Delivered to client
PAID // Payment received
OVERDUE // Past due date, not paid
}
model Invoice {
id String @id @default(cuid())
number String @unique
projectId String
project Project @relation(fields: [projectId])
clientId String
client Client @relation(fields: [clientId])
amount Float
status InvoiceStatus @default(DRAFT)
dueDate DateTime
paidAt DateTime?
stripeLink String? // Stripe payment link URL
sheetsRowId String? // Google Sheets row reference
lineItems LineItem[]
createdAt DateTime @default(now())
}