Self-hosted invoicing for freelancers and small businesses
One deployment. One business. Complete data ownership.
Kivo is an open-source invoicing application designed for business-to-customer workflows. You deploy it once for your business, and it handles everything: clients, invoices, PDFs, emails, payment tracking, and automated reminders.
- Freelancers invoicing clients for project work
- Consultants billing for hours or deliverables
- Small agencies managing multiple client relationships
- Any business that sends invoices and wants to own their data
- No monthly SaaS fees — run it on Cloudflare's generous free tier
- Your data stays yours — invoices, client details, everything lives in your account
- No multi-tenant complexity — one database, one business, no shared anything
- Open source — modify, extend, audit the code
- Client Management — Create, edit, archive clients with full contact details
- Invoice Lifecycle — Draft → Send → View → Pay → Track. Full status history.
- Real PDF Generation — Professional invoices via Cloudflare Browser Rendering API, stored in R2
- Email Delivery — Send invoices, payment reminders, and receipts via Cloudflare Email Service (no API keys needed)
- Online Payments — Stripe Checkout integration for card payments (optional)
- Automatic Reminders — Durable Objects schedule and send payment reminders
- Dashboard — Outstanding totals, monthly revenue, status breakdowns
- Zero Friction — Open the app and start. No signup, no user accounts.
Kivo is a single Cloudflare Worker with all code and frontend assets bundled together:
/— React SPA (dashboard, clients, invoices, settings)/api/*— Hono API routes/health— Health check
Data layer:
- D1 (SQLite) — Clients, invoices, items, events, payments, settings
- R2 (Object Storage) — Generated PDFs, business logos
- Durable Objects — Reminder scheduling engine
This guide walks you through deploying Kivo for your business. You don't need to be a developer, but you should be comfortable running commands in a terminal.
You'll need accounts with these services (all have free tiers):
- Cloudflare — Hosting, database, storage, and email
- Stripe — Online payments (optional)
Install these tools on your computer:
- Node.js 18+
- Wrangler CLI:
npm install -g wrangler
git clone https://github.com/lauragift21/kivo-self-hosted.git
cd kivo-self-hosted
npm installwrangler loginThis opens a browser window to authenticate. Once done, you're connected to your Cloudflare account.
wrangler d1 create kivo-dbAfter creation, Wrangler prints a database_id. Copy it. Open apps/api/wrangler.jsonc and replace:
with the ID you just copied.
wrangler r2 bucket create kivo-storageIn apps/api/wrangler.jsonc, update these fields with your actual domain:
"vars": {
"ENVIRONMENT": "production",
"FRONTEND_URL": "https://invoices.your-domain.com",
"API_URL": "https://invoices.your-domain.com",
"FROM_EMAIL": "[email protected]",
"CF_ACCOUNT_ID": "your-cloudflare-account-id"
}If you don't have a custom domain yet, use the default Workers URL format:
"FRONTEND_URL": "https://kivo.YOUR-SUBDOMAIN.workers.dev",
"API_URL": "https://kivo.YOUR-SUBDOMAIN.workers.dev",You can see your account ID in the Cloudflare dashboard sidebar.
Secrets are sensitive values that shouldn't be in code. Set them with Wrangler:
cd apps/api
# Optional — Stripe keys for online payments
wrangler secret put STRIPE_SECRET_KEY
wrangler secret put STRIPE_WEBHOOK_SECRET
# Optional — Cloudflare API token for PDF generation
wrangler secret put CF_API_TOKEN
# (Create at https://dash.cloudflare.com/profile/api-tokens with "Browser Rendering" permission)Kivo uses Cloudflare's built-in Email Service to send transactional emails. No third-party email provider needed.
Enable email sending for your domain:
npx wrangler email sending enable your-domain.comThe FROM_EMAIL in wrangler.jsonc must use this domain (e.g., [email protected]).
This creates all the tables Kivo needs:
# Run in the project root
cd ../.. # if you're still in apps/api
npm run db:migrate -w apps/apiYou should see: Migrations applied successfully.
npm run deployThis builds the frontend, bundles everything, and deploys to Cloudflare. It'll print a URL like:
https://kivo.YOUR-SUBDOMAIN.workers.dev
You're live.
Open your deployed URL in a browser. You'll be guided through a 5-step onboarding wizard:
- Welcome — Quick intro to what Kivo does
- Business Profile — Business name, email, address, and logo (with live invoice preview)
- Invoice Preferences — Default currency, payment terms, invoice prefix
- First Client — Add your first client (or skip)
- All Set — Celebration screen with quick links to create your first invoice or explore the dashboard
After onboarding, a "Getting Started" checklist appears on the Dashboard until you create your first invoice.
No signup forms. No user accounts. Just start invoicing.
If you want clients to pay invoices by card:
- Create a Stripe account
- Get your Secret Key from the Stripe Dashboard
- Add it as
STRIPE_SECRET_KEY(Step 6) - Go to Developers → Webhooks in Stripe
- Add an endpoint:
- URL:
https://your-domain.workers.dev/api/webhooks/stripe - Events:
checkout.session.completedcheckout.session.expiredpayment_intent.payment_failed
- URL:
- Copy the Webhook Secret and set it as
STRIPE_WEBHOOK_SECRET(Step 6) - Redeploy:
npm run deploy
When you send an invoice, clients will see a "Pay Now" button that takes them to Stripe Checkout.
Instead of the default workers.dev URL, you can use your own domain:
- In Cloudflare Dashboard, go to Workers & Pages
- Select your Kivo worker
- Go to Triggers → Custom Domains
- Add your domain (e.g.,
invoices.your-domain.com) - Update
FRONTEND_URLandAPI_URLinwrangler.jsonc - Redeploy
When a new version is released:
git pull origin main
npm install
npm run db:migrate -w apps/api # if there are new migrations
npm run deploy# Start both API and frontend locally
npm run dev- API: http://localhost:8787
- Web: http://localhost:5173
Apply local database migrations:
npm run db:migrate:local -w apps/api| Secret | Required | Description |
|---|---|---|
STRIPE_SECRET_KEY |
No | Online payments |
STRIPE_WEBHOOK_SECRET |
No | Stripe webhook verification |
CF_API_TOKEN |
No | PDF generation (falls back to HTML if not set) |
Public vars (in wrangler.jsonc):
| Variable | Description |
|---|---|
FRONTEND_URL |
Your app's URL |
API_URL |
Same as above |
FROM_EMAIL |
Sender email (must use domain enabled for Email Service) |
CF_ACCOUNT_ID |
Cloudflare account ID |
Everything lives in your Cloudflare account:
| Data | Location |
|---|---|
| Clients, invoices, settings | D1 (SQLite database) |
| PDFs, logos | R2 (object storage) |
| Reminder schedules | Durable Objects |
Delete the worker and the data is gone. Cloudflare has the data, but only in your account. No third-party SaaS has access.
Emails not sending?
- Check that
FROM_EMAILuses a domain enabled for Cloudflare Email Service:npx wrangler email sending list
- If your domain isn't listed, enable it:
npx wrangler email sending enable your-domain.com - In local development, emails are logged to the console instead of sent (the binding is a stub). Look for
=== EMAIL PREVIEW ===in the wrangler logs.
PDFs not generating?
CF_API_TOKENis optional — without it, Kivo generates HTML instead of PDF- To enable real PDFs, create an API token with "Browser Rendering" permission
Stripe payments not working?
- Verify webhook URL matches your deployed domain exactly
- Check Stripe Dashboard for failed webhook deliveries
- Ensure
STRIPE_SECRET_KEYstarts withsk_test_(test) orsk_live_(production)
Database errors?
- Run migrations:
npm run db:migrate -w apps/api - Check D1 database exists in Cloudflare Dashboard
MIT
