# Project export: BusinessBuddy

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: TreeHacks 2026
- Tagline: Running a business is hard. Now think of your favorite barber, hairdresser, or tattoo artist. BusinessBuddy takes the stress off their shoulders and allows them to only worry about their craft.
- Devpost: https://devpost.com/software/businessbuddy-uehadn
- GitHub: https://github.com/aw03/treehacks2026/
- Demo: https://treehacks2026.vercel.app/
- Video: https://www.youtube.com/embed/aQD4-pKYXxA?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — aw03 (22 commits)

## Devpost submission (written by the team)

### Inspiration

I built BusinessBuddy after seeing friends run service hustles — braiding hair, doing nails, cutting hair — while also being their own scheduler, accountant, and inventory manager. Everything lived in DMs, spreadsheets, or their heads. I wanted to centralize that.

### What it does

BusinessBuddy is an operations dashboard for small service-based businesses. It lets business owners: Manage appointments Track revenue by service Monitor inventory usage View booking trends Generate AI summaries of performance It also provides a simple client-facing view of services. How I built it I built BusinessBuddy as a full-stack web app using: Next.js (App Router) for the frontend and API routes TypeScript for type safety PostgreSQL as the database Prisma ORM for schema modeling and queries Anthropic API for AI-generated summaries and analysis Vercel for deployment I designed a relational schema connecting businesses, services, bookings, and inventory. API routes handle CRUD operations, analytics queries, and AI summarization. The frontend uses server and client components to balance performance and interactivity. Challenges I ran into Designing a flexible but clean database schema Structuring data so analytics queries were efficient Managing environment variables and secure auth correctly Handling schema migrations during rapid iteration Accomplishments that I'm proud of A production-ready relational database design Secure authentication and protected routes AI-powered insights integrated into real business data Full deployment working end-to-end What I learned How to design secure full-stack apps How to model real workflows into relational schemas How important early data structure decisions are How to ship quickly without breaking everything

### What's next

Multi-staff business support Integrated payment processing Deeper financial analytics Automated inventory alerts

## README (from the GitHub repository)

# treehacks2026
# BusinessBuddy

All-in-one app for service-based small businesses!
Built for TreeHacks 2026.  

Creator: aw03

---

## Overview

BusinessBuddy is a business management platform designed for service-based small businesses such as hairstylists, braiders, nail techs, barbers, electricians, and other appointment and service-driven entrepreneurs.

Many of these businesses struggle with:

- Decentralized booking (DMs, texts, spreadsheets)
- Poor inventory tracking
- Limited insight into profitability
- Manual scheduling and client management
- No analytics on service trends or operational costs

BusinessBuddy centralizes scheduling, inventory tracking, and AI-powered business insights into one platform.

---

## Core Features

### Scheduling & Appointments
- Create and manage services
- Log appointments
- Track booking trends
- Monitor revenue

### Inventory Tracking (AI support)
- Chat informally to describe what materials were used. ex "i used around half of the jar of red nail polish"
- Estimate product usage
- Track supply depletion
- Improve restocking decisions

### AI Business Insights
- Summarize trends in appointments
- Identify most profitable services
- Highlight peak and slow periods
- Suggest pricing or operational adjustments

### Analytics Dashboard
- Revenue trends
- Service popularity
- Operational summaries

---

## Tech Stack

### Frontend
- Next.js

### Backend
- Next.js API routes
- Node.js

### Database
- PostgreSQL
- Prisma ORM

### AI & APIs
- Claude Agent SDK (Anthropic) for analytics and summarization
- Poke API for trend summarization/chatbot features

### Deployment
- Vercel

---

## Dependencies

- next
- react
- react-dom
- typescript
- tailwindcss
- prisma
- @prisma/client
- pg
- @anthropic-ai/sdk
- dotenv

Environment variables:

DATABASE_URL=
ANTHROPIC_API_KEY=

---

## Setup

1. Clone the repository

git clone <repo-url>  
cd beauty-biz-app  

2. Install dependencies

npm install  

3. Configure environment variables in `.env`

4. Run database migrations

npx prisma migrate dev  

5. Start the development server

npm run dev  

---

## Hackathon Context

This project was built during TreeHacks 2026 as a fast MVP to explore how AI can empower small service-based businesses with operational intelligence typically reserved for larger companies.

---

## Future Improvements

- Payment integration (Stripe)
- SMS reminders
- Predictive demand forecasting
- Expense tracking
- Native mobile app


## Detected evidence (automated analysis)

Indexed codebase: 48 recognized source files, 157 KB.
- CSS (language) — detected in the code
- Next.js (technology) — detected in the code
- React (technology) — detected in the code
- SQL (language) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- PostgreSQL (technology) — claimed on Devpost, not found in the code
- Vercel (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (56 of 56)

```
beauty-biz-app/.gitignore
beauty-biz-app/app/api/anthropic/models/route.ts
beauty-biz-app/app/api/appointments/[appointmentId]/route.ts
beauty-biz-app/app/api/appointments/route.ts
beauty-biz-app/app/api/auth/mock/route.ts
beauty-biz-app/app/api/bootstrap/route.ts
beauty-biz-app/app/api/businesses/[businessId]/route.ts
beauty-biz-app/app/api/businesses/route.ts
beauty-biz-app/app/api/clients/route.ts
beauty-biz-app/app/api/health/route.ts
beauty-biz-app/app/api/insights/chat/route.ts
beauty-biz-app/app/api/report/metrics/route.ts
beauty-biz-app/app/api/report/summary/route.ts
beauty-biz-app/app/api/services/[id]/route.ts
beauty-biz-app/app/api/services/route.ts
beauty-biz-app/app/api/supplies/[id]/route.ts
beauty-biz-app/app/api/supplies/route.ts
beauty-biz-app/app/api/usage/candidates/route.ts
beauty-biz-app/app/api/usage/instance/[id]/finalize/route.ts
beauty-biz-app/app/api/usage/instance/[id]/route.ts
beauty-biz-app/app/api/usage/instance/route.ts
beauty-biz-app/app/api/usage/parse/route.ts
beauty-biz-app/app/businesses/[businessId]/page.tsx
beauty-biz-app/app/businesses/page.tsx
beauty-biz-app/app/dashboard/_components/requireBusiness.ts
beauty-biz-app/app/dashboard/appointments/page.tsx
beauty-biz-app/app/dashboard/insights/page.tsx
beauty-biz-app/app/dashboard/page.tsx
beauty-biz-app/app/dashboard/report/page.tsx
beauty-biz-app/app/dashboard/services/page.tsx
beauty-biz-app/app/dashboard/supplies/page.tsx
beauty-biz-app/app/dashboard/usage/page.tsx
beauty-biz-app/app/globals.css
beauty-biz-app/app/layout.tsx
beauty-biz-app/app/page.tsx
beauty-biz-app/app/setup/page.tsx
beauty-biz-app/eslint.config.mjs
beauty-biz-app/lib/prisma.ts
beauty-biz-app/lib/walkin.ts
beauty-biz-app/next.config.ts
beauty-biz-app/package.json
beauty-biz-app/postcss.config.mjs
beauty-biz-app/prisma.config.ts.bak
beauty-biz-app/prisma/migrations/20260214221055_init/migration.sql
beauty-biz-app/prisma/migrations/20260214233550_init_business_models/migration.sql
beauty-biz-app/prisma/migrations/20260215030604_add_service_instance_logging/migration.sql
beauty-biz-app/prisma/migrations/20260215034212_add_service_instance_finalize/migration.sql
beauty-biz-app/prisma/migrations/20260215040644_add_instance_times_and_finalize/migration.sql
beauty-biz-app/prisma/migrations/20260215044100_reverted_changes/migration.sql
beauty-biz-app/prisma/migrations/20260215065446_business_walkin_hours/migration.sql
beauty-biz-app/prisma/migrations/migration_lock.toml
beauty-biz-app/prisma/schema.prisma
beauty-biz-app/prisma/seed.ts
beauty-biz-app/README.md
beauty-biz-app/tsconfig.json
README.md
```

### Dependencies

- beauty-biz-app/package.json: @prisma/client@^6.19.2, @tailwindcss/postcss@^4, @types/node@^20, @types/react@^19, @types/react-dom@^19, eslint@^9, eslint-config-next@16.1.6, next@16.1.6, prisma@^6.19.2, react@19.2.3, react-dom@19.2.3, tailwindcss@^4, tsx@^4.21.0, typescript@^5

### Recent commits (newest first)

- make sure vercel updates not only caches
- ensured proper resposne type
- final readme
- working chatbot feature
- force refresh from db
- finalized layout
- fixed up the ui
- changing format layout
- can update status of appointment
- working appointment submit
- working show business
- usage updates and final report
- working update supplies
- updated the schema
- fixed erroros with deleting supplies
- working dashboard subpages
- register, login working
- working login
- fixed vercel deployment issue
- created getter and setters for db

## Key source files (fetched from GitHub, selected and truncated for size)

### beauty-biz-app/package.json

```
{
  "name": "beauty-biz-app",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "postinstall": "prisma generate",
    "build": "next build",
    "start": "next start",
    "lint": "eslint",
    "prisma:generate": "prisma generate",
    "prisma:migrate": "prisma migrate dev",
    "prisma:studio": "prisma studio",
    "db:seed": "tsx prisma/seed.ts"
  },
  "dependencies": {
    "@prisma/client": "^6.19.2",
    "next": "16.1.6",
    "prisma": "^6.19.2",
    "react": "19.2.3",
    "react-dom": "19.2.3"
  },
  "devDependencies": {
    "@tailwindcss/postcss": "^4",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "eslint": "^9",
    "eslint-config-next": "16.1.6",
    "tailwindcss": "^4",
    "tsx": "^4.21.0",
    "typescript": "^5"
  }
}

```

### beauty-biz-app/app/layout.tsx

```typescript
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        {children}
      </body>
    </html>
  );
}

```

### beauty-biz-app/app/page.tsx

```typescript
import Link from "next/link";

export default function Home() {
  return (
    <main className="page-bg">
      <div className="container-narrow">
        <h1>BusinessBuddy</h1>

        <p>
          A smarter way to run and book small business services. Manage appointments,
          track inventory, and grow your business — all in one place.
        </p>

        <div className="stack-24" style={{ marginTop: 28 }}>
          <div style={{ display: "flex", gap: 12, flexWrap: "wrap" }}>
            <Link href="/setup" className="btn btn-primary">
              I’m a Business →
            </Link>

            <Link href="/businesses" className="btn btn-secondary">
              Book a Service →
            </Link>
          </div>

          <div className="card">
            <h2>Built for modern entrepreneurs providing services</h2>
            <ul>
              <li>Centralized scheduling</li>
              <li>Inventory tracking</li>
              <li>Service profitability insights</li>
              <li>AI-powered trend summaries</li>
            </ul>
          </div>
        </div>
      </div>
    </main>
  );
}

```

### beauty-biz-app/app/businesses/page.tsx

```typescript
import Link from "next/link";
import { prisma } from "@/lib/prisma";

export const runtime = "nodejs";
export const dynamic = "force-dynamic";

export default async function BusinessesPage() {
  const businesses = await prisma.business.findMany({
    select: { id: true, name: true, phone: true, walkInEnabled: true },
    orderBy: { createdAt: "desc" },
  });

  return (
    <main className="page-bg">
      <div className="container">
        <h1 style={{ fontSize: 34, margin: 0 }}>All Businesses</h1>
        <div className="small" style={{ marginTop: 10 }}>
          Pick a business to view services and schedule an appointment.
        </div>

        <div
          style={{
            display: "grid",
            gridTemplateColumns: "repeat(auto-fit, minmax(260px, 1fr))",
            gap: 12,
            marginTop: 16,
          }}
        >
          {businesses.map((b) => (
            <div key={b.id} className="card">
              <div style={{ fontWeight: 900, fontSize: 16 }}>{b.name}</div>

              <div className="small" style={{ marginTop: 8 }}>
                {b.phone ?? "No phone listed"}
              </div>

              <div className="small" style={{ marginTop: 10 }}>
                Walk-ins:{" "}
                <span style={{ color: b.walkInEnabled ? "var(--success)" : "var(--muted)", fontWeight: 800 }}>
                  {b.walkInEnabled ? "Enabled" : "Disabled"}
                </span>
              </div>

              <div style={{ marginTop: 14 }}>
                <Link href={`/businesses/${b.id}`} className="btn btn-primary">
                  View business →
                </Link>
              </div>
            </div>
          ))}
        </div>

        {businesses.length === 0 && (
          <div className="small" style={{ marginTop: 16 }}>
            No businesses yet. Create one via your setup flow.
          </div>
        )}

        <div style={{ marginTop: 18 }}>
          <Link href="/" className="btn btn-ghost">
            ← Back home
          </Link>
        </div>
      </div>
    </main>
  );
}

```

### beauty-biz-app/app/setup/page.tsx

```typescript
"use client";

import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";

export default function SetupPage() {
  const router = useRouter();
  const [mode, setMode] = useState<"login" | "register">("register");
  const [email, setEmail] = useState("");
  const [name, setName] = useState("");
  const [msg, setMsg] = useState<string | null>(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    const existing = localStorage.getItem("businessId");
    if (existing) router.push("/dashboard");
  }, [router]);

  async function submit() {
    setLoading(true);
    setMsg(null);
    try {
      const res = await fetch("/api/auth/mock", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ mode, email, name }),
      });
      const data = await res.json();
      if (!res.ok) throw new Error(data?.error ?? "Request failed");

      localStorage.setItem("businessId", data.businessId);
      router.push("/dashboard");
    } catch (e: any) {
      setMsg(e?.message ?? "Unknown error");
    } finally {
      setLoading(false);
    }
  }

  return (
    <main className="page-bg">
      <div className="container" style={{ maxWidth: 720 }}>
        <h1 style={{ fontSize: 34 }}>Business Portal</h1>

        <div className="small" style={{ marginTop: 10 }}>
          Mock auth for hackathon: register or log in with an email to enter the dashboard.
        </div>

        <div className="card" style={{ marginTop: 20 }}>
          {/* Mode Switch */}
          <div style={{ display: "flex", gap: 10, marginBottom: 18 }}>
            <button
              className={mode === "register" ? "btn btn-primary" : "btn btn-secondary"}
              onClick={() => setMode("register")}
              style={{ opacity: mode === "register" ? 1 : 0.85 }}
            >
              Register
            </button>

            <button
              className={mode === "login" ? "btn btn-primary" : "btn btn-secondary"}
              onClick={() => setMode("login")}
              style={{ opacity: mode === "login" ? 1 : 0.85 }}
            >
              Login
            </button>
          </div>

          {mode === "register" && (
            <label className="stack-16" style={{ gap: 8 }}>
              <div className="small" style={{ fontWeight: 900, color: "var(--text)" }}>
                Business name
              </div>
              <input
                value={name}
                onChange={(e) => setName(e.target.value)}
                placeholder="My Salon"
              />
            </label>
          )}

          <label className="stack-16" style={{ gap: 8, marginTop: 14 }}>
            <div className="small" style={{ fontWeight: 900, color: "var(--text)" }}>
              Email
            </div>
            <input
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              placeholder="owner@mysalon.com"
            />
          </label>

          <button
            onClick={submit}
            disabled={loading || !email.trim() || (mode === "register" && !name.trim())}
            className="btn btn-primary"
            style={{ marginTop: 18, opacity: loading ? 0.7 : 1 }}
          >
            {loading ? "Working..." : mode === "login" ? "Login →" : "Register →"}
          </button>

          {msg && (
            <div className="alert alert-danger" style={{ marginTop: 14 }}>
              {msg}
            </div>
          )}
        </div>
      </div>
    </main>
  );
}

```

### beauty-biz-app/app/dashboard/page.tsx

```typescript
"use client";

import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";

export default function DashboardPage() {
  const router = useRouter();
  const [businessId, setBusinessId] = useState<string | null>(null);

  useEffect(() => {
    const id = localStorage.getItem("businessId");
    if (!id) router.push("/setup");
    setBusinessId(id);
  }, [router]);

  function logout() {
    localStorage.removeItem("businessId");
    router.push("/setup");
  }

  return (
    <main className="page-bg">
      <div className="container">
        {/* Top bar */}
        <div
          style={{
            display: "flex",
            justifyContent: "space-between",
            gap: 12,
            flexWrap: "wrap",
            alignItems: "center",
          }}
        >
          <div className="stack-16">
            <h1 style={{ fontSize: 34 }}>Dashboard</h1>
            <div className="small" style={{ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace" }}>
              businessId: {businessId ?? "(loading...)"}
            </div>
          </div>

          <div style={{ display: "flex", gap: 10, flexWrap: "wrap" }}>
            <button className="btn btn-secondary" onClick={() => router.push("/setup")}>
              Account
            </button>
            <button className="btn btn-secondary" onClick={logout}>
              Logout
            </button>
          </div>
        </div>

        {/* Action cards */}
        <div
          style={{
            display: "grid",
            gap: 14,
            gridTemplateColumns: "repeat(auto-fit, minmax(260px, 1fr))",
            marginTop: 22,
          }}
        >
          <ActionCard
            title="Add a service"
            desc="Create services with price + duration."
            onClick={() => router.push("/dashboard/services")}
          />
          <ActionCard
            title="View appointments"
            desc="See upcoming appointments and status."
            onClick={() => router.push("/dashboard/appointments")}
          />
          <ActionCard
            title="Add supplies"
            desc="Track inventory + reorder thresholds."
            onClick={() => router.push("/dashboard/supplies")}
          />
          <ActionCard
            title="Update supplies per service"
            desc="Use chatbot/CV to estimate product usage."
            onClick={() => router.push("/dashboard/usage")}
          />
            <ActionCard
              title="View report"
              desc="Customers, revenue trends, popular services (chatbot summary)."
              onClick={() => router.push("/dashboard/report")}
            />
            <ActionCard
  title="Insights Copilot"
  desc="Interactive AI chat powered by your real metrics and appointment data."
  onClick={() => router.push("/dashboard/insights")}
/>
        </div>
      </div>
    </main>
  );
}

function ActionCard({
  title,
  desc,
  onClick,
}: {
  title: string;
  desc: string;
  onClick: () => void;
}) {
  return (
    <button
      onClick={onClick}
      className="card"
      style={{
        textAlign: "left",
        width: "100%",
        transition: "background 0.15s ease, transform 0.04s ease",
      }}
      onMouseDown={(e) => (e.currentTarget.style.transform = "translateY(1px)")}
      onMouseUp={(e) => (e.currentTarget.style.transform = "translateY(0px)")}
    >
      <div style={{ fontSize: 18, fontWeight: 900, letterSpacing: "-0.01em" }}>
        {title}
      </div>
      <div style={{ marginTop: 6, color: "var(--muted)", lineHeight: 1.5 }}>
        {desc}
      </div>
      <div style={{ marginTop: 12, fontWeight: 800, color: "var(--accent)" }}>
        Open →
      </div>
    </button>
  );
}

```

### beauty-biz-app/app/api/health/route.ts

```typescript
// app/api/health/route.ts
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";

export async function GET() {
  try {
    await prisma.$queryRaw`SELECT 1`;
    return NextResponse.json({ ok: true, db: "connected" });
  } catch (e: any) {
    return NextResponse.json(
      { ok: false, db: "error", error: e?.message ?? "unknown" },
      { status: 500 }
    );
  }
}

```

### beauty-biz-app/app/api/businesses/route.ts

```typescript
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";

export const runtime = "nodejs";

export async function GET() {
  try {
    const businesses = await prisma.business.findMany({
      select: {
        id: true,
        name: true,
        phone: true,
        walkInEnabled: true,
        timezone: true,
        walkInStartMin: true,
        walkInEndMin: true,
      },
      orderBy: { createdAt: "desc" },
    });

    return NextResponse.json({ businesses });
  } catch (e: any) {
    console.error("BUSINESSES_LIST_ERROR:", e);
    return NextResponse.json({ error: e?.message ?? "Failed to load businesses" }, { status: 500 });
  }
}

```

### beauty-biz-app/app/api/bootstrap/route.ts

```typescript
// app/api/bootstrap/route.ts
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";

export async function POST(req: Request) {
  try {
    const body = await req.json().catch(() => ({}));
    const name = typeof body?.name === "string" && body.name.trim() ? body.name.trim() : "Demo Business";
    const email = typeof body?.email === "string" && body.email.trim() ? body.email.trim() : null;
    const phone = typeof body?.phone === "string" && body.phone.trim() ? body.phone.trim() : null;

    const existing = await prisma.business.findFirst();
    if (existing) {
      return NextResponse.json({ businessId: existing.id, created: false });
    }

    const biz = await prisma.business.create({
      data: { name, email, phone },
    });

    return NextResponse.json({ businessId: biz.id, created: true });
  } catch (e: any) {
    return NextResponse.json(
      { error: e?.message ?? "Unknown error" },
      { status: 500 }
    );
  }
}

```

### beauty-biz-app/app/api/clients/route.ts

```typescript
// app/api/clients/route.ts
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";

export async function GET(req: Request) {
  const { searchParams } = new URL(req.url);
  const businessId = searchParams.get("businessId");

  if (!businessId) {
    return NextResponse.json({ error: "Missing businessId" }, { status: 400 });
  }

  const clients = await prisma.client.findMany({
    where: { businessId },
    orderBy: { createdAt: "desc" },
  });

  return NextResponse.json({ clients });
}

export async function POST(req: Request) {
  const body = await req.json().catch(() => null);
  if (!body) return NextResponse.json({ error: "Invalid JSON" }, { status: 400 });

  const { businessId, name, phone, email, notes } = body;

  if (!businessId || typeof businessId !== "string") {
    return NextResponse.json({ error: "Missing businessId" }, { status: 400 });
  }
  if (!name || typeof name !== "string" || !name.trim()) {
    return NextResponse.json({ error: "Missing name" }, { status: 400 });
  }

  const client = await prisma.client.create({
    data: {
      businessId,
      name: name.trim(),
      phone: typeof phone === "string" ? phone.trim() : null,
      email: typeof email === "string" ? email.trim() : null,
      notes: typeof notes === "string" ? notes.trim() : null,
    },
  });

  return NextResponse.json({ client }, { status: 201 });
}

```

[37 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]