Run several side-businesses from one place: orders, inventory, costs, customers, and finances.
Sidekit is for people running more than one small business at the same time. Each business (a "hustle") gets its own research notes, cost sheet with margin math, suppliers, inventory, orders, customers, finances, and tasks. There's also a dashboard that adds everything up across businesses, and a public page customers can use to track their order.
Stack: Next.js 16 (App Router, RSC, Server Actions) · TypeScript 6 strict · Prisma 7 + PostgreSQL · NextAuth v5 · shadcn-style UI on Tailwind v4 · Zustand · React Hook Form + Zod 4 · Recharts 3 · TanStack Table.
- Prerequisites
- Local development setup
- Production deployment (Vercel + Neon)
- Schema changes - how to apply
- Project scripts
- Architecture
- Feature map
- Public API
- Environment variables
- Troubleshooting
| Tool | Version | Notes |
|---|---|---|
| Node.js | ≥ 20 | Tested on Node 22+. |
| pnpm | ≥ 9 | npm i -g pnpm |
| Docker | latest | Optional - used for the local Postgres container. |
| Git | any | - |
| OpenSSL | any | To generate AUTH_SECRET. Most systems have it. |
git clone https://github.com/aneebbaig/sidekit.git
cd sidekit
pnpm installpostinstall runs prisma generate automatically.
cp .env.example .envGenerate an auth secret and paste it into .env as AUTH_SECRET:
openssl rand -base64 32docker compose up -dBoots Postgres 16 on localhost:5433 (non-standard port to avoid colliding with native installs).
- user:
sidekit - password:
sidekit - database:
sidekit
The default DATABASE_URL in .env.example already matches this.
To stop:
docker compose down # stop but keep data
docker compose down -v # stop and delete all dataNo Docker? Use any hosted Postgres (Neon, Supabase, etc.) and set DATABASE_URL to its connection string.
pnpm db:pushpnpm db:seedCreates a demo hustle "Resin Hustle" with sample orders, customers, suppliers, inventory, etc., and the owner account:
email: owner@example.com
password: sidekit123
pnpm devOpen http://localhost:3000. If no account exists yet, you'll be redirected to /setup.
pnpm db:studioOpens a DB browser UI at http://localhost:5555.
- Go to neon.tech → create a project → create a database.
- Copy the two connection strings:
- Pooled → use as
DATABASE_URL - Direct (unpooled) → use as
DATABASE_URL_UNPOOLED
- Pooled → use as
- Vercel Dashboard → Add New… → Project → import
aneebbaig/sidekit. - Framework preset: Next.js (auto-detected).
- Build command: leave default (
prisma generate && next buildruns viapackage.json). - Install command:
pnpm install(auto-detected).
Project → Settings → Environment Variables, add:
| Variable | Value |
|---|---|
DATABASE_URL |
Pooled Neon connection string |
DATABASE_URL_UNPOOLED |
Direct Neon connection string |
AUTH_SECRET |
openssl rand -base64 32 output |
AUTH_TRUST_HOST |
true |
NEXTAUTH_URL |
https://your-deployment.vercel.app |
Set for Production, Preview, and Development scopes as needed.
Run this once from your local machine after setting up .env.production.local with the Neon URLs:
DATABASE_URL_UNPOOLED="<direct neon url>" npx prisma db pushOr with the local production env file:
# .env.production.local already has the Neon URLs
DATABASE_URL_UNPOOLED=$(grep DATABASE_URL_UNPOOLED .env.production.local | cut -d= -f2-) npx prisma db pushPush to main - Vercel auto-deploys. Visit the URL, go to /setup if no account yet.
Whenever you add or modify a field in prisma/schema.prisma:
pnpm db:push
npx prisma generateDATABASE_URL_UNPOOLED="<direct neon url>" npx prisma db pushOr using the local production env file:
DATABASE_URL_UNPOOLED=$(grep DATABASE_URL_UNPOOLED .env.production.local | cut -d= -f2-) npx prisma db pushImportant: Vercel only runs
prisma generate(no DB access needed) at build time. Applying schema changes to the production DB is always a manual step you run locally pointing at the Neon direct URL. If you forget this, the app crashes withP2022 column does not exist.
# 1. Edit prisma/schema.prisma
# 2. Push to local DB + regenerate client
pnpm db:push && npx prisma generate
# 3. Code + test locally
# 4. Push schema to production DB
DATABASE_URL_UNPOOLED="<direct neon url>" npx prisma db push
# 5. Commit and push code to main → Vercel deploys
git add -A && git commit -m "feat: ..." && git push origin main# All work happens on dev
git checkout dev
# When ready to ship
git push origin dev
git checkout main
git merge dev
git push origin main
git checkout devpnpm dev # next dev (starts on :3000)
pnpm build # prisma generate + next build
pnpm start # next start (after build)
pnpm typecheck # tsc --noEmit
pnpm lint # eslint src/
pnpm db:push # push schema to local DB (no migration files)
pnpm db:migrate # create + apply a named migration
pnpm db:seed # run prisma/seed.ts
pnpm db:studio # open Prisma Studio at :5555All
db:*scripts usedotenv-clito load.envautomatically - Prisma 7 no longer reads.envfor CLI commands.
Layered, strict separation of concerns. Data flows one direction only.
UI (Server / Client Components)
↓ call
Server Actions src/actions/*
↓ call
Services src/services/*
↓ call
Repositories src/repositories/*
↓ call
Prisma src/lib/prisma.ts
| Layer | Responsibility |
|---|---|
src/app/ |
Routes, layouts, RSC data fetching, UI |
src/components/ |
Reusable UI primitives (ui/), shared widgets, charts |
src/actions/ |
"use server" entry points returning ActionResult<T> |
src/services/ |
Business logic, validation, orchestration |
src/repositories/ |
Prisma queries - only place that touches the DB |
src/schemas/ |
Zod 4 schemas (single source of truth for shapes) |
src/lib/ |
prisma, result, errors, format, currency, constants |
src/stores/ |
Zustand UI-only state (sidebar collapse, etc.) |
src/auth/ |
NextAuth v5 wiring |
src/generated/ |
Prisma 7 generated client (gitignored - rebuilt on pnpm build) |
prisma.config.ts |
Prisma 7 datasource config (DB URL for CLI tools) |
src/proxy.ts |
Next.js 16 middleware (auth guard, route protection) |
- Strict TypeScript. No
any, noas any. - Every server action returns
{ success: true; data: T } | { success: false; error: string }- never throws to the client. - Every form validates via Zod on both client (
zodResolver) and server (inside the service). - All monetary values stored as
Decimal. Rendered through<Currency />. - All dates rendered through
formatDate/formatDateTime/formatRelative. - Mutations call
revalidatePathinside the action - no client-side cache layer. - Constants live in
src/lib/constants.ts. No magic strings. - Repositories never imported outside services/RSC pages. Services never imported outside actions/RSC pages.
| Module | Route |
|---|---|
| Global dashboard | / |
| Consolidated financials | /financials |
| Hustles list | /hustles |
| Hustle overview | /hustles/[id] |
| Research notes | /hustles/[id]/research |
| Cost sheet + margin calc | /hustles/[id]/cost-sheet |
| Suppliers | /hustles/[id]/suppliers |
| Inventory | /hustles/[id]/inventory |
| Orders | /hustles/[id]/orders |
| Order detail | /hustles/[id]/orders/[orderId] |
| Customers | /hustles/[id]/customers |
| Customer detail | /hustles/[id]/customers/[customerId] |
| Per-hustle financials | /hustles/[id]/financials |
| Tasks (list + kanban) | /hustles/[id]/tasks |
| Settings + danger zone | /hustles/[id]/settings |
| Order tracking (public) | /track/[orderId] |
| Order lookup (public) | /track |
| Setup (first boot only) | /setup |
| Login | /login |
All public endpoints live under /api/public/. No session required - authenticated via x-api-key header (except the lookup endpoint).
Get a hustle's API key from: Settings → API Key → Generate.
Create an order from an external website.
Header: x-api-key: hsk_...
Body:
{
"customerName": "Aisha Khan",
"items": [
{
"name": "Nikah Invitation Set",
"quantity": 50,
"unitPrice": 1200,
"description": "Gold foil, A5"
}
],
"shippingCost": 200,
"discount": 0,
"amountPaid": 0,
"paymentMethod": "BANK_TRANSFER",
"notes": "Deliver by June 10",
"dueDate": "2026-06-10",
"customizations": {
"Bride": "Aisha",
"Groom": "Bilal",
"Date": "15 June 2026"
}
}paymentMethod options: CASH · BANK_TRANSFER · CARD · EASYPAISA · JAZZCASH · OTHER
Response 201:
{
"orderId": "clx...",
"trackUrl": "https://your-domain.com/track/clx..."
}Show trackUrl to the customer after checkout so they can track their order.
Let a customer re-find their tracking link by order number + name. No API key needed - public.
Query params: orderNumber=ORD-0001&customerName=Aisha
Response 200:
{
"orderId": "clx...",
"trackUrl": "https://your-domain.com/track/clx..."
}Response 404: { "error": "Order not found." }
/track/[orderId] - public, no auth. Shows live order status with the hustle's brand color and name applied. Includes a Copy tracking link button and a Look up another order link to /track.
To theme the tracking page for your website: go to Hustles → [your hustle] → Settings → General → set Website URL. The tracking page will then show your brand's color, name, and a back link to your site.
| Variable | Required | Description |
|---|---|---|
DATABASE_URL |
yes | Pooled Postgres connection string (used by the app at runtime). |
DATABASE_URL_UNPOOLED |
yes | Direct Postgres connection string (used by Prisma CLI for migrations). |
AUTH_SECRET |
yes | openssl rand -base64 32. JWT signing key for NextAuth. |
AUTH_TRUST_HOST |
yes | Set true on Vercel / behind a proxy. |
NEXTAUTH_URL |
optional | Public URL. Helps in some preview environments. |
You changed prisma/schema.prisma and deployed, but forgot to run db push against the production DB. Fix:
DATABASE_URL_UNPOOLED="<direct neon url>" npx prisma db pushThen redeploy (or it'll auto-recover on the next request).
Postgres isn't running.
- Local:
docker compose up -d - Production: check the Neon dashboard - the DB may have gone cold. First request wakes it.
GitHub no longer accepts passwords. Use a personal access token or run gh auth login.
Happens when AUTH_SECRET changes between deployments. Set it once in Vercel and never rotate without signing everyone out.
Docker isn't running or the container isn't up. Run docker compose up -d and try again.
If TypeScript complains about a field that exists in the schema, the client is stale. Regenerate:
npx prisma generateUse a pooled URL for DATABASE_URL (pgBouncer / Neon pooler). Use the direct URL only in DATABASE_URL_UNPOOLED for migrations. Never use the direct URL as DATABASE_URL in production.
Recharts colors are in src/components/charts/palette.ts. Tweak there.
Your session cookie has a stale user ID. Clear authjs.* cookies in browser devtools and sign back in.
MIT. Use, modify, and run as you please.