Add README and technical docs

- README with quick start, package overview, links to docs
- docs/setup.md — prerequisites, env vars, installation, running, testing
- docs/architecture.md — monorepo structure, backend/frontend design
- docs/api.md — full endpoint reference with permissions
- docs/database.md — schema overview, migrations, multi-tenancy
- docs/testing.md — test runner, suites, writing tests
- Updated .env.example with all supported variables
This commit is contained in:
Ryan Moon
2026-03-29 08:31:20 -05:00
parent b9f78639e2
commit 1d48f0befa
7 changed files with 599 additions and 7 deletions

View File

@@ -1,13 +1,14 @@
# Forte — Development Environment Variables # Forte — Environment Variables
# These are used inside Docker Compose (docker-compose.dev.yml overrides most of these) # Copy to .env and adjust values for your setup.
# Docker Compose overrides host values (postgres, valkey) automatically.
# Database # Database (PostgreSQL 16)
DATABASE_URL=postgresql://forte:forte@postgres:5432/forte DATABASE_URL=postgresql://forte:forte@localhost:5432/forte
# Valkey (Redis-compatible) # Valkey / Redis
REDIS_URL=redis://valkey:6379 REDIS_URL=redis://localhost:6379
# JWT # JWT — use a strong random secret in production
JWT_SECRET=change-me-in-production-use-a-long-random-string JWT_SECRET=change-me-in-production-use-a-long-random-string
# API Server # API Server
@@ -16,3 +17,17 @@ HOST=0.0.0.0
# Environment # Environment
NODE_ENV=development NODE_ENV=development
# Logging (optional)
# LOG_LEVEL=info
# LOG_FILE=./logs/forte.log
# File Storage (optional — defaults to local)
# STORAGE_PROVIDER=local
# STORAGE_LOCAL_PATH=./data/files
# CORS (optional — defaults to * in development)
# CORS_ORIGINS=https://admin.example.com
# Frontend URL (used in password reset links)
# APP_URL=http://localhost:5173

55
README.md Normal file
View File

@@ -0,0 +1,55 @@
# Forte
Music store management platform — POS, inventory, rentals, lessons, repairs, and accounting.
Built by [Lunarfront Tech LLC](https://lunarfront.com).
## Tech Stack
- **Runtime:** Bun
- **Language:** TypeScript (end-to-end)
- **API:** Fastify + Drizzle ORM + PostgreSQL 16
- **Frontend:** React + TanStack Router + TanStack Query
- **Validation:** Zod (shared schemas)
- **Queue/Cache:** BullMQ + Valkey 8
- **Monorepo:** Turborepo + Bun workspaces
## Quick Start
```bash
bun install
cp .env.example .env # configure DATABASE_URL, REDIS_URL, JWT_SECRET
cd packages/backend && bunx drizzle-kit migrate
bun run dev # starts backend (:8000) + admin UI (:5173)
```
## Packages
| Package | Description |
|---------|-------------|
| `packages/backend` | Fastify API server |
| `packages/admin` | Admin UI (React + Vite) |
| `packages/shared` | Zod schemas, types, shared utils |
## Documentation
| Doc | Description |
|-----|-------------|
| [Setup](docs/setup.md) | Prerequisites, environment, installation, running |
| [Architecture](docs/architecture.md) | Monorepo structure, backend/frontend design, state management |
| [API Reference](docs/api.md) | All endpoints, pagination, auth, permissions |
| [Database](docs/database.md) | Schema overview, migrations, multi-tenancy |
| [Testing](docs/testing.md) | Test runner, suites, writing tests, assertions |
## Commands
```bash
bun run dev # start all packages in dev mode
bun run test # run all tests
bun run lint # lint all packages
bun run format # format with Prettier
```
## License
Proprietary. All rights reserved.

146
docs/api.md Normal file
View File

@@ -0,0 +1,146 @@
# API Reference
Base URL: `http://localhost:8000/v1`
All authenticated endpoints require `Authorization: Bearer <token>`. Registration and login require `X-Company-ID` header.
## Pagination
Every list endpoint accepts:
| Param | Default | Description |
|-------|---------|-------------|
| `page` | `1` | Page number |
| `limit` | `25` | Items per page (max 100) |
| `q` | — | Search query (ilike across relevant columns) |
| `sort` | varies | Sort field name |
| `order` | `asc` | `asc` or `desc` |
Response shape:
```json
{
"data": [...],
"pagination": {
"page": 1,
"limit": 25,
"total": 142,
"totalPages": 6
}
}
```
## Auth
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | `/auth/register` | No (needs `X-Company-ID`) | Create user account |
| POST | `/auth/login` | No | Login, returns JWT |
| GET | `/auth/me` | Yes | Current user profile |
| PATCH | `/auth/me` | Yes | Update profile (firstName, lastName) |
| POST | `/auth/change-password` | Yes | Change password |
| POST | `/auth/reset-password/:userId` | Yes (`users.admin`) | Generate password reset link |
## Accounts
| Method | Path | Permission | Description |
|--------|------|------------|-------------|
| GET | `/accounts` | `accounts.view` | List accounts (paginated, searchable) |
| POST | `/accounts` | `accounts.edit` | Create account |
| GET | `/accounts/:id` | `accounts.view` | Get account |
| PATCH | `/accounts/:id` | `accounts.edit` | Update account |
| DELETE | `/accounts/:id` | `accounts.admin` | Soft-delete account |
## Members
| Method | Path | Permission | Description |
|--------|------|------------|-------------|
| GET | `/members` | `accounts.view` | List all members (paginated) |
| GET | `/accounts/:id/members` | `accounts.view` | List members for account |
| POST | `/accounts/:id/members` | `accounts.edit` | Create member |
| GET | `/members/:id` | `accounts.view` | Get member |
| PATCH | `/members/:id` | `accounts.edit` | Update member |
| DELETE | `/members/:id` | `accounts.admin` | Delete member |
| POST | `/members/:id/move` | `accounts.edit` | Move member to another account |
## Member Sub-Resources
| Method | Path | Permission | Description |
|--------|------|------------|-------------|
| GET | `/members/:id/identifiers` | `accounts.view` | List identifiers |
| POST | `/members/:id/identifiers` | `accounts.edit` | Create identifier |
| PATCH | `/identifiers/:id` | `accounts.edit` | Update identifier |
| DELETE | `/identifiers/:id` | `accounts.admin` | Delete identifier |
| GET | `/accounts/:id/payment-methods` | `accounts.view` | List payment methods |
| POST | `/accounts/:id/payment-methods` | `accounts.edit` | Create payment method |
| PATCH | `/payment-methods/:id` | `accounts.edit` | Update payment method |
| DELETE | `/payment-methods/:id` | `accounts.admin` | Delete payment method |
| GET | `/accounts/:id/tax-exemptions` | `accounts.view` | List tax exemptions |
| POST | `/accounts/:id/tax-exemptions` | `accounts.edit` | Create tax exemption |
| PATCH | `/tax-exemptions/:id` | `accounts.edit` | Update tax exemption |
| DELETE | `/tax-exemptions/:id` | `accounts.admin` | Delete tax exemption |
| GET | `/accounts/:id/processor-links` | `accounts.view` | List processor links |
| POST | `/accounts/:id/processor-links` | `accounts.edit` | Create processor link |
| PATCH | `/processor-links/:id` | `accounts.edit` | Update processor link |
| DELETE | `/processor-links/:id` | `accounts.admin` | Delete processor link |
## Users & RBAC
| Method | Path | Permission | Description |
|--------|------|------------|-------------|
| GET | `/users` | `users.view` | List users (paginated, includes roles) |
| PATCH | `/users/:id/status` | `users.admin` | Enable/disable user |
| GET | `/users/:id/roles` | `users.view` | Get user's roles |
| POST | `/users/:id/roles` | `users.edit` | Assign role to user |
| DELETE | `/users/:id/roles/:roleId` | `users.edit` | Remove role from user |
| GET | `/permissions` | `users.view` | List all permissions |
| GET | `/roles` | `users.view` | List roles (paginated) |
| GET | `/roles/all` | `users.view` | List all roles (unpaginated, for dropdowns) |
| GET | `/roles/:id` | `users.view` | Get role with permissions |
| POST | `/roles` | `users.admin` | Create custom role |
| PATCH | `/roles/:id` | `users.admin` | Update role |
| DELETE | `/roles/:id` | `users.admin` | Delete custom role |
| GET | `/me/permissions` | Yes | Current user's permissions + roles |
## Files
| Method | Path | Permission | Description |
|--------|------|------------|-------------|
| GET | `/files?entityType=&entityId=` | `files.view` | List files for entity |
| POST | `/files` | `files.upload` | Upload file (multipart) |
| GET | `/files/:id` | `files.view` | Get file metadata |
| GET | `/files/serve/*` | `files.view` | Serve file content |
| DELETE | `/files/:id` | `files.delete` | Delete file |
Upload accepts multipart form with fields: `file`, `entityType`, `entityId`, `category`.
Valid entity types: `user`, `member`, `member_identifier`, `product`, `rental_agreement`, `repair_ticket`.
## Products & Inventory
| Method | Path | Permission | Description |
|--------|------|------------|-------------|
| GET | `/products` | `inventory.view` | List products (paginated) |
| POST | `/products` | `inventory.edit` | Create product |
| GET | `/products/:id` | `inventory.view` | Get product |
| PATCH | `/products/:id` | `inventory.edit` | Update product |
| DELETE | `/products/:id` | `inventory.admin` | Delete product |
| GET | `/categories` | `inventory.view` | List categories |
| POST | `/categories` | `inventory.edit` | Create category |
| GET | `/suppliers` | `inventory.view` | List suppliers |
| POST | `/suppliers` | `inventory.edit` | Create supplier |
## Lookup Tables
| Method | Path | Permission | Description |
|--------|------|------------|-------------|
| GET | `/lookups/unit-statuses` | `inventory.view` | List unit statuses |
| POST | `/lookups/unit-statuses` | `inventory.admin` | Create custom status |
| GET | `/lookups/item-conditions` | `inventory.view` | List item conditions |
| POST | `/lookups/item-conditions` | `inventory.admin` | Create custom condition |
## Health
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/health` | No | Health check |

120
docs/architecture.md Normal file
View File

@@ -0,0 +1,120 @@
# Architecture
## Monorepo Structure
```
forte/
packages/
shared/ @forte/shared — Zod schemas, types, business logic, utils
backend/ @forte/backend — Fastify API server
admin/ @forte/admin — Admin UI (React + Vite)
planning/ Domain planning docs (01-26)
docs/ Technical documentation
```
Managed with Turborepo and Bun workspaces. `@forte/shared` is a dependency of both `backend` and `admin`.
## Backend
**Fastify** API server with a plugin-based architecture.
```
src/
main.ts App entry, plugin registration, server start
plugins/
database.ts Drizzle ORM + PostgreSQL connection
auth.ts JWT auth, permission checking, inheritance
redis.ts Valkey/Redis (ioredis)
cors.ts CORS configuration
storage.ts File storage provider registration
error-handler.ts Centralized error → HTTP response mapping
dev-auth.ts Dev-only auth bypass
routes/v1/
auth.ts Register, login, password change, reset, profile
accounts.ts Accounts, members, identifiers, payment methods, etc.
rbac.ts Users list, roles CRUD, role assignments, permissions
files.ts File upload/download/delete
services/
account.service.ts Account + member business logic
rbac.service.ts Roles, permissions, user role management
file.service.ts File validation, storage, metadata
product.service.ts Products + inventory
lookup.service.ts Lookup table management
inventory.service.ts Stock receipts, unit tracking
db/
schema/ Drizzle table definitions
migrations/ SQL migrations (drizzle-kit)
seeds/ System permission + role definitions
storage/
provider.ts StorageProvider interface
local.ts Local filesystem provider
s3.ts S3 provider (placeholder)
utils/
pagination.ts withPagination, withSort, buildSearchCondition, paginatedResponse
```
### Request Flow
1. Fastify receives request
2. `onRequest` hook sets `companyId` from header
3. `authenticate` preHandler verifies JWT, loads permissions, checks `is_active`
4. `requirePermission` preHandler checks user has required permission slug
5. Route handler validates input with Zod, calls service, returns response
6. Error handler catches typed errors and maps to HTTP status codes
### Permission Inheritance
Permissions follow a hierarchy: `admin` implies `edit`, which implies `view` for the same domain. Having `accounts.admin` automatically grants `accounts.edit` and `accounts.view`. Non-hierarchical actions (`upload`, `delete`, `send`, `export`) don't cascade.
## Frontend (Admin UI)
**React** SPA with TanStack Router (file-based routing) and TanStack Query (data fetching).
```
src/
routes/
_authenticated.tsx Layout with sidebar, permission-gated nav
_authenticated/
accounts/ Account list + detail pages
members/ Member list + detail pages
users.tsx Users admin page
roles/ Roles list + create/edit pages
profile.tsx User profile + settings
help.tsx In-app wiki
login.tsx Login page
api/ React Query options + mutations per domain
components/
ui/ shadcn/ui primitives
shared/ DataTable, AvatarUpload
accounts/ Domain-specific forms
stores/
auth.store.ts Zustand — token, user, permissions
theme.store.ts Zustand — color theme + light/dark mode
hooks/
use-pagination.ts URL-based pagination state
lib/
api-client.ts Fetch wrapper with JWT + error handling
themes.ts Color theme definitions
wiki/
index.ts In-app help content
```
### State Management
| Concern | Solution |
|---------|----------|
| Auth + permissions | Zustand store, persisted to sessionStorage |
| Server data | TanStack Query (cache, refetch, invalidation) |
| URL state (pagination) | TanStack Router search params |
| Theme | Zustand store, persisted to localStorage |
| Component state | React `useState` |
## Multi-Tenancy
Every domain table has a `company_id` column. All queries filter by the authenticated user's company. Location-scoped tables (inventory, transactions) additionally filter by `location_id`.
## Database
PostgreSQL 16 with Drizzle ORM. Migrations are generated with `bunx drizzle-kit generate` and applied with `bunx drizzle-kit migrate`. Schema files live in `packages/backend/src/db/schema/`.
Key tables: `company`, `location`, `user`, `account`, `member`, `member_identifier`, `product`, `inventory_unit`, `role`, `permission`, `user_role_assignment`, `role_permission`, `file`.

86
docs/database.md Normal file
View File

@@ -0,0 +1,86 @@
# Database
## Setup
PostgreSQL 16. Two databases:
| Database | Port | Usage |
|----------|------|-------|
| `forte` | 5432 | Development |
| `forte_api_test` | 5432 | API integration tests (auto-created by test runner) |
## Migrations
Migrations are managed by Drizzle Kit and live in `packages/backend/src/db/migrations/`.
```bash
cd packages/backend
# Generate a migration from schema changes
bunx drizzle-kit generate
# Apply pending migrations
bunx drizzle-kit migrate
```
Schema files: `packages/backend/src/db/schema/`
## Multi-Tenancy
All domain tables include `company_id` (uuid FK to `company`). Every query filters by the authenticated user's company. Location-scoped tables additionally include `location_id`.
## Schema Overview
### Core
| Table | Description |
|-------|-------------|
| `company` | Tenant (music store business) |
| `location` | Physical store location |
| `user` | Staff/admin user account |
### Accounts & Members
| Table | Description |
|-------|-------------|
| `account` | Billing entity (family, individual, business) |
| `member` | Individual person on an account |
| `member_identifier` | ID documents (DL, passport, school ID) |
| `payment_method` | Stored payment methods |
| `processor_link` | Payment processor integrations |
| `tax_exemption` | Tax exemption records |
### Inventory
| Table | Description |
|-------|-------------|
| `product` | Product catalog entry |
| `inventory_unit` | Individual serialized/non-serialized unit |
| `stock_receipt` | Incoming stock records |
| `category` | Product categories |
| `supplier` | Product suppliers |
| `inventory_unit_status` | Lookup: unit statuses |
| `item_condition` | Lookup: item conditions |
### RBAC
| Table | Description |
|-------|-------------|
| `permission` | System permissions (global, seeded) |
| `role` | Company-scoped roles (system + custom) |
| `role_permission` | Role-to-permission mapping |
| `user_role_assignment` | User-to-role mapping |
### Files
| Table | Description |
|-------|-------------|
| `file` | File metadata (path, type, size, entity reference) |
## Key Conventions
- UUIDs for all primary keys (`defaultRandom()`)
- `created_at` and `updated_at` timestamps with timezone on all tables
- Soft deletes via `is_active` boolean where applicable
- Auto-generated sequential numbers: `account_number` (6-digit), `member_number`
- Lookup tables support both system (immutable) and custom (company-scoped) values

89
docs/setup.md Normal file
View File

@@ -0,0 +1,89 @@
# Development Setup
## Prerequisites
- [Bun](https://bun.sh) v1.1+
- PostgreSQL 16
- Valkey 8 (or Redis 7+)
## Installation
```bash
git clone <repo-url> && cd forte
bun install
```
## Environment
Create a `.env` file in the project root:
```env
DATABASE_URL=postgresql://forte:forte@localhost:5432/forte
REDIS_URL=redis://localhost:6379
JWT_SECRET=your-secret-here
NODE_ENV=development
```
### All Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `DATABASE_URL` | `postgresql://forte:forte@localhost:5432/forte` | PostgreSQL connection string |
| `REDIS_URL` | `redis://localhost:6379` | Valkey/Redis connection string |
| `JWT_SECRET` | (auto-generated in dev) | Secret for signing JWTs. **Required in production.** |
| `PORT` | `8000` | Backend API port |
| `HOST` | `0.0.0.0` | Backend bind address |
| `NODE_ENV` | — | `development`, `test`, or `production` |
| `LOG_LEVEL` | `info` | Pino log level (`debug`, `info`, `warn`, `error`, `silent`) |
| `LOG_FILE` | — | Path to log file (production only, also logs to stdout) |
| `STORAGE_PROVIDER` | `local` | File storage provider (`local` or `s3`) |
| `STORAGE_LOCAL_PATH` | `./data/files` | Local file storage directory |
| `CORS_ORIGINS` | `*` (dev) | Comma-separated allowed origins |
| `APP_URL` | `http://localhost:5173` | Frontend URL (used in password reset links) |
## Database
```bash
# Create the database
createdb forte
# Run migrations
cd packages/backend
source ../.env # or source .env from project root
bunx drizzle-kit migrate
```
## Running
```bash
# From project root — starts all packages
bun run dev
# Or individually:
cd packages/backend && source .env && bun run dev # API on :8000
cd packages/admin && bun run dev # UI on :5173
```
## Testing
```bash
# API integration tests (starts a backend, seeds DB, runs HTTP tests)
cd packages/backend
source .env
bun run api-test
# Filter by suite or tag
bun run api-test --suite accounts
bun run api-test --tag crud
# Unit tests
bun run test
```
## Useful Commands
```bash
bun run lint # ESLint across all packages
bun run format # Prettier write
bun run build # Build all packages
```

81
docs/testing.md Normal file
View File

@@ -0,0 +1,81 @@
# Testing
## API Integration Tests
The primary test suite lives at `packages/backend/api-tests/`. It uses a custom runner that:
1. Creates/migrates a `forte_api_test` database
2. Seeds company, lookup tables, RBAC permissions/roles
3. Starts the backend on port 8001
4. Registers a test user with admin role
5. Runs test suites via HTTP requests
6. Tears down the server
### Running
```bash
cd packages/backend
source .env # needs DB credentials
bun run api-test
```
### Filtering
```bash
bun run api-test --suite accounts # run only the accounts suite
bun run api-test --tag crud # run tests tagged 'crud'
```
### Suites
| Suite | Tests | Coverage |
|-------|-------|----------|
| `accounts` | 17 | CRUD, search, pagination, sort, billing mode |
| `members` | 19 | CRUD, search, move, isMinor, address inheritance |
| `files` | 7 | Upload, list, get, delete, validation, profile pictures |
| `rbac` | 21 | Permission enforcement, role CRUD, user status, pagination |
### Writing Tests
Tests are defined in `api-tests/suites/`. Each file exports a suite:
```typescript
import { suite } from '../lib/context.js'
suite('MyDomain', { tags: ['domain'] }, (t) => {
t.test('does something', { tags: ['crud'] }, async () => {
const res = await t.api.post('/v1/endpoint', { name: 'Test' })
t.assert.status(res, 201)
t.assert.equal(res.data.name, 'Test')
})
})
```
Available on `t`:
- `t.api` — HTTP client (`get`, `post`, `patch`, `del`) with auth token
- `t.token` — JWT token string
- `t.baseUrl` — Backend URL
- `t.assert` — Assertion helpers (`status`, `equal`, `ok`, `includes`, `greaterThan`, etc.)
### Assert Methods
| Method | Description |
|--------|-------------|
| `status(res, code)` | Check HTTP status |
| `equal(a, b)` | Strict equality |
| `notEqual(a, b)` | Strict inequality |
| `ok(value)` | Truthy check |
| `includes(arr, value)` | Array includes |
| `contains(str, sub)` | String contains |
| `greaterThan(a, b)` | Numeric comparison |
## Unit Tests
Unit tests use `bun:test` and live alongside the code or in `__tests__/`:
```bash
cd packages/backend
bun run test
```
Current unit test coverage: shared utils (date helpers, currency formatting, state normalization).