Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# CellarSync Environment Configuration
# Copy this file to .env and fill in values

# Application
NODE_ENV=development
PORT=3001
HOST=0.0.0.0

# Database
# Path to SQLite database file (created automatically)
DATABASE_PATH=./data/cellar-dev.db

# JWT Authentication
# Generate secrets with: node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
JWT_SECRET=change-me-to-a-random-64-char-string
JWT_REFRESH_SECRET=change-me-to-another-random-64-char-string
JWT_EXPIRY=15m
JWT_REFRESH_EXPIRY=7d

# CORS
# Set to your domain in production, e.g., https://cellarsync.yourdomain.com
CORS_ORIGIN=http://localhost:5173

# Backup (production only)
# BACKUP_DIR=/var/backups/cellarsync
76 changes: 76 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: CI

on:
pull_request:
branches: [main]
push:
branches: [main]

jobs:
lint-and-typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm run typecheck

unit-tests:
runs-on: ubuntu-latest
needs: lint-and-typecheck
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm run test:unit -- --coverage || echo "No unit tests yet"
- uses: actions/upload-artifact@v4
if: always()
with:
name: unit-coverage
path: coverage/
retention-days: 7

functional-tests:
runs-on: ubuntu-latest
needs: lint-and-typecheck
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm run test:functional -- --coverage || echo "No functional tests yet"
- uses: actions/upload-artifact@v4
if: always()
with:
name: functional-coverage
path: coverage/
retention-days: 7

e2e-tests:
runs-on: ubuntu-latest
needs: [unit-tests, functional-tests]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npx playwright install --with-deps chromium || echo "Playwright not configured yet"
- run: npm run build || echo "Build not configured yet"
- run: npm run test:e2e || echo "No e2e tests yet"
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: apps/web/playwright-report/
retention-days: 7
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ vendor/
dist/
build/
out/
*.tsbuildinfo

# Environment variables
.env
Expand All @@ -28,3 +29,13 @@ npm-debug.log*

# Coverage
coverage/

# Database files
data/

# Drizzle
.drizzle/

# Playwright
playwright-report/
test-results/
10 changes: 10 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
node_modules
dist
build
out
coverage
data
.drizzle
apps/api/drizzle
playwright-report
test-results
111 changes: 84 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,40 +1,97 @@
# example-app-claude
# CellarSync

A project documenting the process of building an example application entirely using Claude Code. This repo serves as a reference for workflows, patterns, and practices when developing software with AI-assisted coding through Claude Code.
Wine collection inventory management system for personal and small group use.

## AI-Assisted Development Workflow
**Tech Stack:** React 19 + Vite | Fastify | SQLite + Drizzle ORM | GitHub Actions CI/CD

This project follows a structured, AI-driven workflow from concept to implementation:
## Quick Start

```bash
# Prerequisites: Node.js >= 20, npm >= 10

# Install dependencies
npm install

# Set up environment variables
cp .env.example .env

# Run database migrations and seed with sample data
npm run db:migrate
npm run db:seed

# Start development servers (API + Web concurrently)
npm run dev
```

- **API:** http://localhost:3001
- **Web:** http://localhost:5173 (proxies `/api` to the API server)

## Test Accounts

| Role | Email | Password |
|--------|----------------------------|------------------|
| Admin | admin@cellarsync.local | adminpassword1 |
| Member | member@cellarsync.local | memberpassword1 |

## Available Scripts

| Script | Description |
|----------------------|------------------------------------------------|
| `npm run dev` | Start API and Web dev servers concurrently |
| `npm run build` | Build web app for production |
| `npm run lint` | Run ESLint across all packages |
| `npm run lint:fix` | Run ESLint with auto-fix |
| `npm run typecheck` | TypeScript type checking across all packages |
| `npm run test` | Run all tests |
| `npm run test:unit` | Run unit tests only |
| `npm run test:functional` | Run functional/API tests |
| `npm run test:e2e` | Run Playwright end-to-end tests |
| `npm run test:coverage` | Run tests with coverage report |
| `npm run db:migrate` | Run database migrations |
| `npm run db:seed` | Seed database with sample data |
| `npm run db:studio` | Open Drizzle Studio (visual DB browser) |
| `npm run format` | Format code with Prettier |
| `npm run format:check` | Check code formatting |

## Project Structure

```
1. Create PRD
Define the product requirements document with full technical
architecture, functional specs, and delivery milestones.
2. Generate GitHub Issues from PRD
Break the PRD down into trackable, actionable GitHub issues
organized by phase and labeled by category.
3. Generate Roadmap from GitHub Issues
Produce a comprehensive roadmap that maps issues to milestones,
defines dependencies, and establishes delivery timelines.
4. Implement (issue by issue)
Work through the roadmap, using GitHub issues as the unit of
work, with CI/CD validating each change.
cellarsync/
├── packages/
│ └── shared/ # Zod schemas, TypeScript types, constants
├── apps/
│ ├── api/ # Fastify backend (port 3001)
│ │ ├── src/
│ │ │ ├── routes/ # API route handlers
│ │ │ ├── db/ # Drizzle schema, migrations, seed
│ │ │ ├── services/ # Business logic
│ │ │ ├── plugins/ # Fastify plugins (JWT, etc.)
│ │ │ └── middleware/
│ │ ├── drizzle/ # Migration files
│ │ └── tests/
│ └── web/ # React SPA (port 5173)
│ ├── src/
│ └── tests/
├── docs/
│ ├── PRD.md # Product requirements document
│ └── ROADMAP.md # Development roadmap
└── data/ # SQLite databases (gitignored)
```

Each step is executed with Claude Code, producing artifacts that feed into the next stage. The goal is a fully traceable path from product idea to deployed software.
## API Endpoints

## Project: CellarSync
```
GET /api/health # Health check
POST /api/auth/login # Login (returns JWT tokens)
POST /api/auth/refresh # Refresh access token
POST /api/auth/logout # Invalidate refresh token
```

A wine collection inventory management system for personal and small group use. See [docs/PRD.md](docs/PRD.md) for the full product requirements document.
## Documentation

**Tech Stack:** React SPA | Fastify | SQLite | GCP VM | GitHub Actions CI/CD
- [Product Requirements Document](docs/PRD.md)
- [Development Roadmap](docs/ROADMAP.md)

## License

This project is licensed under the Apache License 2.0. See [LICENSE](LICENSE) for details.
Apache License 2.0 — see [LICENSE](LICENSE).
10 changes: 10 additions & 0 deletions apps/api/drizzle.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { defineConfig } from 'drizzle-kit';

export default defineConfig({
schema: './src/db/schema.ts',
out: './drizzle',
dialect: 'sqlite',
dbCredentials: {
url: process.env.DATABASE_PATH || './data/cellar-dev.db',
},
});
102 changes: 102 additions & 0 deletions apps/api/drizzle/0000_natural_vengeance.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
CREATE TABLE `bottles` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`wine_id` integer NOT NULL,
`storage_location` text,
`purchase_date` text,
`purchase_price` real,
`purchase_currency` text DEFAULT 'USD',
`purchase_source` text,
`status` text DEFAULT 'in_stock' NOT NULL,
`consumed_date` text,
`added_by` integer NOT NULL,
`created_at` text DEFAULT (datetime('now')) NOT NULL,
`updated_at` text DEFAULT (datetime('now')) NOT NULL,
FOREIGN KEY (`wine_id`) REFERENCES `wines`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`added_by`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE INDEX `idx_bottles_wine_id` ON `bottles` (`wine_id`);--> statement-breakpoint
CREATE INDEX `idx_bottles_status` ON `bottles` (`status`);--> statement-breakpoint
CREATE INDEX `idx_bottles_storage` ON `bottles` (`storage_location`);--> statement-breakpoint
CREATE TABLE `refresh_tokens` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`user_id` integer NOT NULL,
`token_hash` text NOT NULL,
`family_id` text NOT NULL,
`revoked` integer DEFAULT false NOT NULL,
`expires_at` text NOT NULL,
`created_at` text DEFAULT (datetime('now')) NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE INDEX `idx_refresh_tokens_user` ON `refresh_tokens` (`user_id`);--> statement-breakpoint
CREATE INDEX `idx_refresh_tokens_family` ON `refresh_tokens` (`family_id`);--> statement-breakpoint
CREATE UNIQUE INDEX `idx_refresh_tokens_hash` ON `refresh_tokens` (`token_hash`);--> statement-breakpoint
CREATE TABLE `storage_locations` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`name` text NOT NULL,
`parent_id` integer,
`description` text,
`capacity` integer,
`created_at` text DEFAULT (datetime('now')) NOT NULL,
FOREIGN KEY (`parent_id`) REFERENCES `storage_locations`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `tasting_notes` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`bottle_id` integer NOT NULL,
`user_id` integer NOT NULL,
`tasted_date` text NOT NULL,
`rating` integer,
`appearance` text,
`nose` text,
`palate` text,
`finish` text,
`overall_notes` text,
`occasion` text,
`created_at` text DEFAULT (datetime('now')) NOT NULL,
`updated_at` text DEFAULT (datetime('now')) NOT NULL,
FOREIGN KEY (`bottle_id`) REFERENCES `bottles`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE INDEX `idx_tasting_notes_bottle` ON `tasting_notes` (`bottle_id`);--> statement-breakpoint
CREATE INDEX `idx_tasting_notes_user` ON `tasting_notes` (`user_id`);--> statement-breakpoint
CREATE INDEX `idx_tasting_notes_rating` ON `tasting_notes` (`rating`);--> statement-breakpoint
CREATE TABLE `users` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`email` text NOT NULL,
`password_hash` text NOT NULL,
`display_name` text NOT NULL,
`role` text DEFAULT 'member' NOT NULL,
`created_at` text DEFAULT (datetime('now')) NOT NULL,
`updated_at` text DEFAULT (datetime('now')) NOT NULL
);
--> statement-breakpoint
CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`);--> statement-breakpoint
CREATE TABLE `wines` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`name` text NOT NULL,
`producer` text NOT NULL,
`region` text NOT NULL,
`sub_region` text,
`country` text NOT NULL,
`vintage` integer,
`varietal` text NOT NULL,
`color` text NOT NULL,
`bottle_size` text DEFAULT '750ml' NOT NULL,
`alcohol_pct` real,
`drink_from` integer,
`drink_to` integer,
`notes` text,
`created_by` integer NOT NULL,
`created_at` text DEFAULT (datetime('now')) NOT NULL,
`updated_at` text DEFAULT (datetime('now')) NOT NULL,
FOREIGN KEY (`created_by`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE INDEX `idx_wines_producer` ON `wines` (`producer`);--> statement-breakpoint
CREATE INDEX `idx_wines_region` ON `wines` (`region`);--> statement-breakpoint
CREATE INDEX `idx_wines_country` ON `wines` (`country`);--> statement-breakpoint
CREATE INDEX `idx_wines_vintage` ON `wines` (`vintage`);--> statement-breakpoint
CREATE INDEX `idx_wines_color` ON `wines` (`color`);
Loading