+ );
+}
+```
+
+## Best Practices
+
+### DO
+
+- Keep components lean and focused on rendering
+- Move business logic to stores, primitives, or utilities
+- Access props directly (never destructure)
+- Use store imports instead of prop drilling
+- Use import aliases
+- Use Ark UI components from `@corates/ui`
+- Use `solid-icons` for icons
+- Handle errors appropriately
+- Use Tailwind CSS for styling
+
+### DON'T
+
+- Don't destructure props
+- Don't prop-drill application state
+- Don't put business logic in components
+- Don't use emojis (use icons)
+- Don't import UI components from local files (use `@corates/ui`)
+- Don't create "God components" that do too much
+- Don't use more than 5 props (consider store or context)
+
+## Component Examples
+
+### Simple Component
+
+```jsx
+import { Show } from 'solid-js';
+import projectStore from '@/stores/projectStore.js';
+
+export default function ProjectList() {
+ const projects = () => projectStore.getProjectList();
+ const loading = () => projectStore.isProjectListLoading();
+
+ return (
+
+ Loading...
}>
+ {project => }
+
+
+ );
+}
+```
+
+### Form Component
+
+```jsx
+import { createSignal } from 'solid-js';
+import { createFormErrorSignals } from '@/lib/form-errors.js';
+import { handleFetchError } from '@/lib/error-utils.js';
+import projectActionsStore from '@/stores/projectActionsStore';
+
+export default function CreateProjectForm(props) {
+ const [name, setName] = createSignal('');
+ const errors = createFormErrorSignals(createSignal);
+
+ async function handleSubmit(e) {
+ e.preventDefault();
+
+ try {
+ await projectActionsStore.createProject({
+ name: name(),
+ });
+ props.onSuccess?.();
+ } catch (error) {
+ errors.handleError(error);
+ }
+ }
+
+ return (
+
+ );
+}
+```
+
+## Related Guides
+
+- [State Management Guide](/guides/state-management) - For store patterns
+- [Primitives Guide](/guides/primitives) - For reusable logic patterns
+- [Style Guide](/guides/style-guide) - For UI/UX guidelines
+- [Error Handling Guide](/guides/error-handling) - For error handling patterns
diff --git a/packages/docs/guides/configuration.md b/packages/docs/guides/configuration.md
new file mode 100644
index 000000000..08466c235
--- /dev/null
+++ b/packages/docs/guides/configuration.md
@@ -0,0 +1,222 @@
+# Configuration Guide
+
+This guide covers configuration files, environment variables, path aliases, and setup for CoRATES.
+
+## Overview
+
+CoRATES uses a monorepo structure with multiple packages, each with its own configuration. This guide covers the key configuration files and settings.
+
+## Package Structure
+
+The project is organized as a monorepo with packages under `packages/`:
+
+```
+packages/
+├── web/ # Frontend application (SolidJS)
+├── workers/ # Backend API (Cloudflare Workers)
+├── landing/ # Landing/marketing site
+├── ui/ # Shared UI component library
+├── shared/ # Shared TypeScript utilities
+├── mcp/ # MCP server for development tools
+└── docs/ # Documentation site
+```
+
+## Path Aliases
+
+### Frontend (Web Package)
+
+Path aliases are defined in `packages/web/jsconfig.json`:
+
+```1:20:packages/web/jsconfig.json
+{
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["src/*"],
+ "@components/*": ["src/components/*"],
+ "@auth-ui/*": ["src/components/auth-ui/*"],
+ "@checklist-ui/*": ["src/components/checklist-ui/*"],
+ "@project-ui/*": ["src/components/project-ui/*"],
+ "@routes/*": ["src/routes/*"],
+ "@primitives/*": ["src/primitives/*"],
+ "@auth/*": ["src/components/auth-ui/*"],
+ "@offline/*": ["src/offline/*"],
+ "@api/*": ["src/api/*"],
+ "@config/*": ["src/config/*"],
+ "@lib/*": ["src/lib/*"]
+ }
+ },
+ "exclude": ["node_modules", "dist"]
+}
+```
+
+**Usage:**
+
+```js
+// Use aliases instead of relative paths
+import projectStore from '@/stores/projectStore.js';
+import SignIn from '@auth-ui/SignIn.jsx';
+import { useProject } from '@primitives/useProject/index.js';
+```
+
+**Available Aliases:**
+
+- `@/*` → `src/*`
+- `@components/*` → `src/components/*`
+- `@auth-ui/*` → `src/components/auth-ui/*`
+- `@checklist-ui/*` → `src/components/checklist-ui/*`
+- `@project-ui/*` → `src/components/project-ui/*`
+- `@routes/*` → `src/routes/*`
+- `@primitives/*` → `src/primitives/*`
+- `@api/*` → `src/api/*`
+- `@config/*` → `src/config/*`
+- `@lib/*` → `src/lib/*`
+
+## Environment Variables
+
+### Backend (Workers)
+
+Environment variables are defined in `wrangler.jsonc` or `.dev.vars`:
+
+**Required:**
+
+- `DB` - D1 database binding
+- `BETTER_AUTH_SECRET` - Secret key for Better Auth
+- `BETTER_AUTH_URL` - Base URL for Better Auth callbacks
+
+**Optional:**
+
+- `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` - Google OAuth
+- `ORCID_CLIENT_ID` / `ORCID_CLIENT_SECRET` - ORCID OAuth
+- `POSTMARK_API_KEY` - Email service
+- `STRIPE_SECRET_KEY` - Stripe billing
+- `R2_BUCKET` - R2 storage binding for PDFs
+
+### Frontend (Web)
+
+Environment variables are typically set at build time via Vite:
+
+- `VITE_API_BASE` - API base URL (defaults to `/api`)
+
+## Build Configuration
+
+### Vite Config (Frontend)
+
+The web package uses Vite for building. Configuration in `packages/web/vite.config.js`.
+
+### Wrangler Config (Backend)
+
+Cloudflare Workers configuration in `packages/workers/wrangler.jsonc`:
+
+- D1 database bindings
+- Durable Object bindings
+- R2 bucket bindings
+- Environment variables
+- Routes
+
+## Development Setup
+
+### Prerequisites
+
+- Node.js (v18+)
+- pnpm (package manager)
+- Cloudflare account (for Workers/D1)
+
+### Installation
+
+```bash
+# Install dependencies
+pnpm install
+
+# Setup local development
+# See README.md for detailed setup instructions
+```
+
+### Development Commands
+
+```bash
+# Start development servers
+pnpm dev
+
+# Build all packages
+pnpm build
+
+# Run tests
+pnpm test
+
+# Run linting
+pnpm lint
+```
+
+## Package-Specific Configuration
+
+### Web Package
+
+- **Build tool**: Vite
+- **Framework**: SolidJS + SolidStart
+- **Styling**: Tailwind CSS
+- **Type checking**: TypeScript (via JSDoc comments)
+
+### Workers Package
+
+- **Runtime**: Cloudflare Workers
+- **Database**: Cloudflare D1 (SQLite)
+- **ORM**: Drizzle ORM
+- **Auth**: Better Auth
+- **Storage**: Cloudflare R2
+
+### UI Package
+
+- **Components**: Ark UI
+- **Icons**: solid-icons
+- **TypeScript**: Full TypeScript support
+
+## Import Patterns
+
+### UI Components
+
+Always import from `@corates/ui`:
+
+```js
+import { Dialog, Select, Toast } from '@corates/ui';
+```
+
+### Icons
+
+Import from `solid-icons`:
+
+```js
+import { BiRegularHome } from 'solid-icons/bi';
+import { FiUsers } from 'solid-icons/fi';
+```
+
+### Internal Packages
+
+Import using package names:
+
+```js
+import { createDomainError } from '@corates/shared';
+```
+
+## Best Practices
+
+### DO
+
+- Use path aliases instead of relative paths
+- Keep configuration files in sync across packages
+- Use environment variables for secrets
+- Document required environment variables
+- Use package.json scripts for common tasks
+
+### DON'T
+
+- Don't use relative paths when aliases are available
+- Don't hardcode API URLs or secrets
+- Don't commit `.env` files
+- Don't create circular dependencies between packages
+
+## Related Guides
+
+- [Development Workflow Guide](/guides/development-workflow) - For setup and common tasks
+- [API Development Guide](/guides/api-development) - For backend configuration
+- [Component Development Guide](/guides/components) - For frontend configuration
diff --git a/packages/docs/guides/database.md b/packages/docs/guides/database.md
new file mode 100644
index 000000000..ec2ce8a6e
--- /dev/null
+++ b/packages/docs/guides/database.md
@@ -0,0 +1,613 @@
+# Database Guide
+
+This guide covers the database schema, Drizzle ORM patterns, migrations, and query patterns in CoRATES.
+
+## Overview
+
+CoRATES uses Cloudflare D1 (SQLite) with Drizzle ORM for type-safe database operations. All database interactions must use Drizzle - raw SQL queries are not allowed.
+
+## Schema Overview
+
+The database schema is defined in `packages/workers/src/db/schema.js` using Drizzle's SQLite schema builder.
+
+### Core Tables
+
+#### Users
+
+```1:24:packages/workers/src/db/schema.js
+import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
+import { sql } from 'drizzle-orm';
+
+// Users table
+export const user = sqliteTable('user', {
+ id: text('id').primaryKey(),
+ name: text('name').notNull(),
+ email: text('email').notNull().unique(),
+ emailVerified: integer('emailVerified', { mode: 'boolean' }).default(false),
+ image: text('image'),
+ createdAt: integer('createdAt', { mode: 'timestamp' }).default(sql`(unixepoch())`),
+ updatedAt: integer('updatedAt', { mode: 'timestamp' }).default(sql`(unixepoch())`),
+ username: text('username').unique(),
+ displayName: text('displayName'),
+ avatarUrl: text('avatarUrl'),
+ role: text('role'), // Better Auth admin/plugin role (e.g. 'user', 'admin')
+ persona: text('persona'), // optional: researcher, student, librarian, other
+ profileCompletedAt: integer('profileCompletedAt'), // unix timestamp (seconds)
+ twoFactorEnabled: integer('twoFactorEnabled', { mode: 'boolean' }).default(false),
+ // Admin plugin fields
+ banned: integer('banned', { mode: 'boolean' }).default(false),
+ banReason: text('banReason'),
+ banExpires: integer('banExpires', { mode: 'timestamp' }),
+});
+```
+
+#### Projects
+
+```72:82:packages/workers/src/db/schema.js
+// Projects table (for user's research projects)
+export const projects = sqliteTable('projects', {
+ id: text('id').primaryKey(),
+ name: text('name').notNull(),
+ description: text('description'),
+ createdBy: text('createdBy')
+ .notNull()
+ .references(() => user.id, { onDelete: 'cascade' }),
+ createdAt: integer('createdAt', { mode: 'timestamp' }).default(sql`(unixepoch())`),
+ updatedAt: integer('updatedAt', { mode: 'timestamp' }).default(sql`(unixepoch())`),
+});
+```
+
+#### Project Members
+
+```84:95:packages/workers/src/db/schema.js
+// Project membership table (which users have access to which projects)
+export const projectMembers = sqliteTable('project_members', {
+ id: text('id').primaryKey(),
+ projectId: text('projectId')
+ .notNull()
+ .references(() => projects.id, { onDelete: 'cascade' }),
+ userId: text('userId')
+ .notNull()
+ .references(() => user.id, { onDelete: 'cascade' }),
+ role: text('role').default('member'), // owner, collaborator, member, viewer
+ joinedAt: integer('joinedAt', { mode: 'timestamp' }).default(sql`(unixepoch())`),
+});
+```
+
+### Table Relationships
+
+- **users** ↔ **projects**: One-to-many (createdBy)
+- **users** ↔ **project_members**: Many-to-many (through projectMembers table)
+- **projects** ↔ **project_members**: One-to-many
+
+## Drizzle ORM Patterns
+
+### Database Client
+
+Always create DB client from environment:
+
+```js
+import { createDb } from '../db/client.js';
+
+async c => {
+ const db = createDb(c.env.DB);
+ // Use db
+};
+```
+
+### Query Patterns
+
+#### Select Single Record
+
+```js
+import { eq } from 'drizzle-orm';
+
+const project = await db.select().from(projects).where(eq(projects.id, projectId)).get();
+```
+
+#### Select Multiple Records
+
+```js
+const allProjects = await db.select().from(projects).where(eq(projects.createdBy, userId)).all();
+```
+
+#### Select with Joins
+
+```js
+import { eq, and } from 'drizzle-orm';
+
+const projectWithMembers = await db
+ .select({
+ project: projects,
+ member: projectMembers,
+ })
+ .from(projects)
+ .innerJoin(projectMembers, eq(projects.id, projectMembers.projectId))
+ .where(eq(projects.id, projectId))
+ .all();
+```
+
+#### Count Records
+
+```js
+import { count } from 'drizzle-orm';
+
+const [result] = await db.select({ count: count() }).from(projects).where(eq(projects.createdBy, userId));
+
+const projectCount = result?.count || 0;
+```
+
+#### Insert Records
+
+```js
+const newProject = await db
+ .insert(projects)
+ .values({
+ id: crypto.randomUUID(),
+ name: 'My Project',
+ description: 'Project description',
+ createdBy: userId,
+ })
+ .returning()
+ .get();
+```
+
+#### Update Records
+
+```js
+await db
+ .update(projects)
+ .set({
+ name: 'Updated Name',
+ updatedAt: new Date(),
+ })
+ .where(eq(projects.id, projectId));
+```
+
+#### Delete Records
+
+```js
+await db.delete(projects).where(eq(projects.id, projectId));
+```
+
+### Batch Operations
+
+**Always use `db.batch()` for related operations that must be atomic:**
+
+```js
+// CORRECT - Atomic operations
+const batchOps = [
+ db.insert(projects).values({ id, name, createdBy }),
+ db.insert(projectMembers).values({ projectId: id, userId, role: 'owner' }),
+];
+await db.batch(batchOps);
+
+// WRONG - Not atomic
+await db.insert(projects).values({ id, name });
+await db.insert(projectMembers).values({ projectId: id, userId });
+```
+
+Use batch when operations must succeed or fail together. Single independent operations don't need batch.
+
+### Where Conditions
+
+Combine conditions with `and`, `or`:
+
+```js
+import { and, or, eq, like } from 'drizzle-orm';
+
+// AND condition
+const result = await db
+ .select()
+ .from(projects)
+ .where(and(eq(projects.id, projectId), eq(projects.createdBy, userId)))
+ .get();
+
+// OR condition
+const results = await db
+ .select()
+ .from(projects)
+ .where(
+ or(
+ eq(projects.createdBy, userId),
+ // User is member via join
+ ),
+ )
+ .all();
+
+// LIKE (string search)
+const results = await db
+ .select()
+ .from(projects)
+ .where(like(projects.name, `%${searchTerm}%`))
+ .all();
+```
+
+## Schema Management
+
+### Architecture: Single Source of Truth
+
+The database schema follows a single source of truth pattern:
+
+```
+Drizzle Schema (src/db/schema.js)
+ ↓
+Migration SQL (migrations/0001_init.sql)
+ ↓
+Test SQL Constant (src/__tests__/migration-sql.js) [generated]
+```
+
+**Key Principle:** Only the Drizzle schema (`src/db/schema.js`) needs to be maintained manually. Everything else is generated from it.
+
+### Files Overview
+
+#### Source Files (Maintained Manually)
+
+- **`src/db/schema.js`** - Drizzle ORM schema definitions
+ - This is the single source of truth
+ - All table definitions, columns, relationships, and constraints are defined here
+
+- **`migrations/0001_init.sql`** - SQL migration file
+ - Can be manually maintained OR generated using `drizzle-kit generate`
+ - Used by Wrangler to apply migrations to D1 databases
+
+#### Generated Files (Auto-generated)
+
+- **`src/__tests__/migration-sql.js`** - Test SQL constant
+ - Generated by `scripts/generate-test-sql.mjs`
+ - Exported as `MIGRATION_SQL` constant
+ - Used by test helpers for database reset
+ - **Do not edit manually** - it will be overwritten
+
+## Migrations
+
+### Migration Process
+
+**All migrations go in a single file: `packages/workers/migrations/0001_init.sql`**
+
+Do NOT create separate migration files (0002_xxx.sql, etc.). Edit the existing 0001_init.sql file directly.
+
+### Migration Structure
+
+Migrations use standard SQLite syntax:
+
+```sql
+-- Create projects table
+CREATE TABLE IF NOT EXISTS projects (
+ id TEXT PRIMARY KEY,
+ name TEXT NOT NULL,
+ description TEXT,
+ createdBy TEXT NOT NULL,
+ createdAt INTEGER DEFAULT (unixepoch()),
+ updatedAt INTEGER DEFAULT (unixepoch()),
+ FOREIGN KEY (createdBy) REFERENCES user(id) ON DELETE CASCADE
+);
+
+-- Create indexes
+CREATE INDEX IF NOT EXISTS idx_projects_created_by ON projects(createdBy);
+CREATE INDEX IF NOT EXISTS idx_projects_updated_at ON projects(updatedAt);
+```
+
+### Migration Workflow
+
+#### Step 1: Update Drizzle Schema
+
+Edit `src/db/schema.js` to add/modify tables, columns, or constraints:
+
+```typescript
+export const myTable = sqliteTable('my_table', {
+ id: text('id').primaryKey(),
+ name: text('name').notNull(),
+ // ... other columns
+});
+```
+
+#### Step 2: Update Migration SQL
+
+You have two options:
+
+**Option A: Manual Update (Current Approach)**
+
+- Edit `migrations/0001_init.sql` directly
+- Add/update CREATE TABLE statements, indexes, etc.
+- Keep it in sync with the Drizzle schema
+
+**Option B: Use Drizzle Kit**
+
+```bash
+pnpm db:generate
+```
+
+- This generates migration files in `migrations/` directory
+- Review and apply the generated SQL
+- For this project, you may need to merge into `0001_init.sql`
+
+#### Step 3: Regenerate Test SQL Constant
+
+After updating the migration SQL:
+
+```bash
+pnpm db:generate:test
+```
+
+This ensures test helpers use the latest schema.
+
+#### Step 4: Apply Migration to Database
+
+**Local development:**
+
+```bash
+pnpm db:migrate
+```
+
+**Production:**
+
+```bash
+pnpm db:migrate:prod
+```
+
+### Available Scripts
+
+#### Generate Migration SQL from Schema
+
+```bash
+pnpm db:generate
+```
+
+This runs `drizzle-kit generate` which:
+
+- Reads the Drizzle schema from `src/db/schema.js`
+- Compares it with existing migrations
+- Generates new migration SQL files in `migrations/`
+
+**Note:** Currently, the project uses a single consolidated migration file (`0001_init.sql`). When making schema changes, you can either:
+
+1. Manually edit `migrations/0001_init.sql` to match the schema
+2. Use `drizzle-kit generate` and merge the generated SQL into `0001_init.sql`
+
+#### Generate Test SQL Constant
+
+```bash
+pnpm db:generate:test
+```
+
+This runs `scripts/generate-test-sql.mjs` which:
+
+- Reads `migrations/0001_init.sql`
+- Generates `src/__tests__/migration-sql.js` with the SQL as an exported constant
+- Used by test helpers to reset the database schema in tests
+
+**When to run:** After updating the migration SQL file, run this to update the test helpers.
+
+## Indexes
+
+Create indexes for frequently queried columns:
+
+```sql
+-- Index on foreign keys
+CREATE INDEX IF NOT EXISTS idx_project_members_project_id ON project_members(projectId);
+CREATE INDEX IF NOT EXISTS idx_project_members_user_id ON project_members(userId);
+
+-- Index on searchable fields
+CREATE INDEX IF NOT EXISTS idx_projects_name ON projects(name);
+
+-- Unique indexes (enforced by schema)
+CREATE UNIQUE INDEX IF NOT EXISTS idx_user_email ON user(email);
+```
+
+## Common Query Patterns
+
+### Check Existence
+
+```js
+const exists = await db.select({ id: projects.id }).from(projects).where(eq(projects.id, projectId)).get();
+
+if (!exists) {
+ // Project doesn't exist
+}
+```
+
+### Pagination
+
+```js
+import { desc, limit, offset } from 'drizzle-orm';
+
+const page = 1;
+const pageSize = 20;
+
+const results = await db
+ .select()
+ .from(projects)
+ .where(eq(projects.createdBy, userId))
+ .orderBy(desc(projects.createdAt))
+ .limit(pageSize)
+ .offset((page - 1) * pageSize)
+ .all();
+```
+
+### Update with Current Timestamp
+
+```js
+await db
+ .update(projects)
+ .set({
+ name: newName,
+ updatedAt: new Date(),
+ })
+ .where(eq(projects.id, projectId));
+```
+
+### Upsert Pattern
+
+```js
+// Check if exists, then insert or update
+const existing = await db.select().from(projects).where(eq(projects.id, projectId)).get();
+
+if (existing) {
+ await db.update(projects).set({ name: newName }).where(eq(projects.id, projectId));
+} else {
+ await db.insert(projects).values({ id: projectId, name: newName, createdBy: userId });
+}
+```
+
+## Data Types
+
+### Timestamps
+
+Timestamps are stored as integers (Unix epoch in seconds):
+
+```js
+createdAt: integer('createdAt', { mode: 'timestamp' }).default(sql`(unixepoch())`);
+```
+
+Convert between Date and integer:
+
+```js
+// Store
+const now = Math.floor(Date.now() / 1000); // seconds
+await db.insert(projects).values({ createdAt: new Date() }); // Drizzle converts
+
+// Read
+const project = await db.select().from(projects).get();
+const date = project.createdAt; // Date object (if mode: 'timestamp')
+```
+
+### Booleans
+
+Booleans stored as integers (0/1):
+
+```js
+emailVerified: integer('emailVerified', { mode: 'boolean' }).default(false);
+```
+
+Drizzle automatically converts between boolean and integer.
+
+## Test Helpers
+
+Test seed functions use Drizzle ORM and Zod validation:
+
+### Seed Functions
+
+All seed functions (`seedUser`, `seedProject`, `seedProjectMember`, `seedSession`, `seedSubscription`) now:
+
+- Use Drizzle ORM `insert()` operations (no raw SQL)
+- Validate inputs with Zod schemas (`src/__tests__/seed-schemas.js`)
+- Automatically convert timestamps and handle type transformations
+
+### Example Usage
+
+```javascript
+import { seedUser, seedProject } from './helpers.js';
+
+// Timestamps can be Date objects or Unix timestamps (seconds)
+await seedUser({
+ id: 'user-1',
+ name: 'Test User',
+ email: 'test@example.com',
+ createdAt: Math.floor(Date.now() / 1000), // Unix timestamp
+ updatedAt: Math.floor(Date.now() / 1000),
+ role: 'researcher', // optional, defaults to 'researcher'
+ emailVerified: true, // boolean or 0/1
+});
+
+await seedProject({
+ id: 'project-1',
+ name: 'Test Project',
+ description: 'A test project',
+ createdBy: 'user-1',
+ createdAt: Math.floor(Date.now() / 1000),
+ updatedAt: Math.floor(Date.now() / 1000),
+});
+```
+
+### Validation
+
+All seed function parameters are validated using Zod schemas:
+
+- Required fields are enforced
+- Email format is validated
+- Enums (roles, tiers, statuses) are validated
+- Timestamps accept both Date objects and Unix timestamps
+- Boolean fields accept both boolean and number (0/1) values
+
+## Configuration
+
+### Drizzle Kit Config
+
+`drizzle.config.ts`:
+
+```typescript
+export default defineConfig({
+ dialect: 'sqlite',
+ schema: './src/db/schema.js',
+ out: './migrations',
+});
+```
+
+### Dependencies
+
+- **`drizzle-orm`** - ORM for database operations (runtime)
+- **`drizzle-kit`** - CLI tool for migrations (dev dependency)
+
+## Best Practices
+
+### DO
+
+- Always use Drizzle ORM queries (never raw SQL)
+- Use `db.batch()` for atomic operations
+- Create indexes for frequently queried columns
+- Use transactions for related operations
+- Handle errors gracefully
+- Use proper types (text, integer, etc.)
+- Always update the schema first - Edit `src/db/schema.js` before migration SQL
+- Keep migrations in sync - Ensure `migrations/0001_init.sql` matches the schema
+- Regenerate test SQL - Run `pnpm db:generate:test` after migration changes
+- Use Drizzle ORM in tests - Seed functions use Drizzle, not raw SQL
+- Validate inputs - All seed functions validate with Zod schemas
+
+### DON'T
+
+- Don't use raw SQL queries
+- Don't skip batch operations for related writes
+- Don't forget indexes on foreign keys
+- Don't store timestamps as strings (use integer with timestamp mode)
+- Don't forget to handle null/undefined values
+
+## Troubleshooting
+
+### Test SQL is out of sync
+
+If tests fail with schema errors:
+
+```bash
+pnpm db:generate:test
+```
+
+### Migration SQL doesn't match schema
+
+1. Review `src/db/schema.js` for the intended schema
+2. Update `migrations/0001_init.sql` to match
+3. Run `pnpm db:generate:test` to update test helpers
+
+### Seed function validation errors
+
+Check the Zod schema in `src/__tests__/seed-schemas.js`:
+
+- Required fields must be provided
+- Enums must match valid values
+- Timestamps can be Date objects or Unix timestamps (seconds)
+
+## Related Files
+
+- `src/db/schema.js` - Drizzle schema definitions
+- `src/db/client.js` - Drizzle client factory
+- `src/__tests__/helpers.js` - Test utilities and seed functions
+- `src/__tests__/seed-schemas.js` - Zod validation schemas for seed functions
+- `src/__tests__/migration-sql.js` - Generated test SQL constant (do not edit)
+- `scripts/generate-test-sql.mjs` - Script to generate test SQL constant
+- `drizzle.config.ts` - Drizzle Kit configuration
+
+## Related Guides
+
+- [API Development Guide](/guides/api-development) - For database usage in routes
+- [Architecture Diagrams](/architecture/diagrams/04-data-model) - For entity relationships
diff --git a/packages/docs/guides/development-workflow.md b/packages/docs/guides/development-workflow.md
new file mode 100644
index 000000000..0a686e704
--- /dev/null
+++ b/packages/docs/guides/development-workflow.md
@@ -0,0 +1,270 @@
+# Development Workflow Guide
+
+This guide covers getting started, development commands, code organization, and common development tasks.
+
+## Getting Started
+
+### Prerequisites
+
+- **Node.js** v18 or higher
+- **pnpm** (package manager) - Install via `npm install -g pnpm`
+- **Cloudflare account** (for Workers/D1 development)
+- **Git** (for version control)
+
+### Installation
+
+```bash
+# Clone the repository
+git clone
+cd corates
+
+# Install dependencies
+pnpm install
+
+# Setup local development environment
+# See package.json scripts for available commands
+```
+
+### Initial Setup
+
+1. **Environment Variables**: Copy `.env.example` to `.env` and fill in required values
+2. **Database**: Create local D1 database via Wrangler
+3. **Run Migrations**: Apply database migrations
+4. **Start Dev Server**: Run `pnpm dev` to start development servers
+
+## Development Commands
+
+### Package Scripts
+
+From the root directory:
+
+```bash
+# Development
+pnpm dev # Start all development servers
+pnpm dev:web # Start frontend dev server
+pnpm dev:workers # Start backend dev server
+pnpm dev:docs # Start docs dev server
+
+# Building
+pnpm build # Build all packages
+pnpm build:web # Build frontend
+pnpm build:workers # Build backend
+pnpm build:docs # Build docs
+
+# Testing
+pnpm test # Run all tests
+pnpm test:watch # Run tests in watch mode
+pnpm lint # Lint all packages
+pnpm lint:fix # Lint and fix issues
+
+# Database
+pnpm db:migrate # Run database migrations (local)
+pnpm db:generate # Generate migrations from schema
+```
+
+### Package-Specific Commands
+
+Each package has its own scripts in `packages/*/package.json`:
+
+```bash
+# From package directory
+cd packages/web
+pnpm dev # Start dev server for this package
+pnpm build # Build this package
+pnpm test # Run tests for this package
+```
+
+## Code Organization
+
+### File Structure
+
+```
+packages/
+├── web/ # Frontend application
+│ └── src/
+│ ├── components/ # UI components
+│ ├── stores/ # State management
+│ ├── primitives/ # Reusable hooks
+│ ├── routes/ # Routes
+│ ├── lib/ # Utilities
+│ └── config/ # Configuration
+├── workers/ # Backend API
+│ └── src/
+│ ├── routes/ # API routes
+│ ├── db/ # Database schema
+│ ├── middleware/ # Middleware
+│ ├── auth/ # Authentication
+│ └── config/ # Configuration
+├── ui/ # Shared UI components
+└── shared/ # Shared utilities
+```
+
+### Naming Conventions
+
+- **Components**: PascalCase (e.g., `ProjectCard.jsx`)
+- **Stores**: camelCase with `Store` suffix (e.g., `projectStore.js`)
+- **Primitives**: camelCase with `use` prefix (e.g., `useProject.js`)
+- **Utilities**: camelCase (e.g., `errorUtils.js`)
+- **Constants**: UPPER_SNAKE_CASE (e.g., `API_BASE`)
+
+### Code Comments
+
+Comments should explain **why**, not **what**:
+
+```js
+// GOOD - explains why
+// Some APIs occasionally return 500s on valid requests. We retry up to 3 times
+// before surfacing an error.
+retries += 1;
+
+// BAD - narrates what the code does
+retries += 1; // Increment retries counter
+```
+
+## Git Workflow
+
+### Branch Naming
+
+- `feature/description` - New features
+- `fix/description` - Bug fixes
+- `docs/description` - Documentation changes
+- `refactor/description` - Code refactoring
+
+### Commit Messages
+
+Follow conventional commits:
+
+```
+feat: add project creation form
+fix: handle offline state in project store
+docs: update API development guide
+refactor: simplify error handling
+```
+
+### Pull Requests
+
+- Keep PRs focused and small
+- Include description of changes
+- Reference related issues
+- Ensure tests pass
+- Update documentation if needed
+
+## Common Development Tasks
+
+### Adding a New API Route
+
+1. Create route file in `packages/workers/src/routes/`
+2. Add validation schema in `packages/workers/src/config/validation.js`
+3. Add route to main app in `packages/workers/src/index.js`
+4. Write tests in `packages/workers/src/routes/__tests__/`
+5. Update API documentation if needed
+
+### Adding a New Component
+
+1. Create component file in appropriate directory under `packages/web/src/components/`
+2. Use path aliases for imports
+3. Follow component patterns (see [Component Guide](/guides/components))
+4. Add to barrel export if in a feature directory
+5. Write tests if component has complex logic
+
+### Adding a New Store
+
+1. Create store file in `packages/web/src/stores/`
+2. Create corresponding actions store if needed
+3. Export as singleton
+4. Document store structure in comments
+5. Use in components via direct imports
+
+### Adding Database Schema Changes
+
+1. Update schema in `packages/workers/src/db/schema.js`
+2. Update migration SQL in `packages/workers/migrations/0001_init.sql`
+3. Regenerate test SQL: `pnpm db:generate:test`
+4. Run migrations locally: `pnpm db:migrate`
+5. Test changes with database operations
+
+### Debugging
+
+#### Frontend Debugging
+
+- Use browser DevTools
+- Check SolidJS DevTools extension
+- Inspect store state in console
+- Use `console.log` for debugging (remove before commit)
+
+#### Backend Debugging
+
+- Use `console.log` in Workers (visible in Wrangler logs)
+- Check D1 database via Wrangler CLI
+- Use `wrangler tail` for real-time logs
+- Test routes with curl or Postman
+
+## Common Issues and Solutions
+
+### Issue: Tests failing with database errors
+
+**Solution:** Ensure database is reset between tests:
+
+```js
+beforeEach(async () => {
+ await resetTestDatabase();
+});
+```
+
+### Issue: Import aliases not working
+
+**Solution:** Check `jsconfig.json` paths are correct, restart dev server
+
+### Issue: Durable Object state persisting between tests
+
+**Solution:** This is expected with `isolatedStorage: false`. Reset database instead.
+
+### Issue: CORS errors in development
+
+**Solution:** Check CORS middleware configuration in `packages/workers/src/index.js`
+
+### Issue: Build errors after dependency updates
+
+**Solution:** Clear node_modules and reinstall:
+
+```bash
+rm -rf node_modules packages/*/node_modules
+pnpm install
+```
+
+## Best Practices
+
+### DO
+
+- Run tests before committing
+- Lint code before committing
+- Write tests for new features
+- Update documentation when adding features
+- Use TypeScript types/JSDoc for better IDE support
+- Follow existing code patterns
+- Keep components small and focused
+- Use stores for shared state
+
+### DON'T
+
+- Don't commit console.log statements
+- Don't skip tests
+- Don't ignore linting errors
+- Don't prop-drill state (use stores)
+- Don't destructure props in SolidJS components
+- Don't use raw SQL (use Drizzle ORM)
+- Don't create circular dependencies
+
+## Resources
+
+- [Contributing Guide](/.github/Contributing.md) - Detailed contribution guidelines
+- [Architecture Diagrams](/architecture/) - System architecture
+- [Error Handling Guide](/guides/error-handling) - Error handling patterns
+- [Style Guide](/guides/style-guide) - UI/UX guidelines
+
+## Related Guides
+
+- [Configuration Guide](/guides/configuration) - Configuration details
+- [Testing Guide](/guides/testing) - Testing patterns
+- [API Development Guide](/guides/api-development) - Backend development
+- [Component Development Guide](/guides/components) - Frontend development
diff --git a/packages/docs/guides/error-handling.md b/packages/docs/guides/error-handling.md
new file mode 100644
index 000000000..875f545a4
--- /dev/null
+++ b/packages/docs/guides/error-handling.md
@@ -0,0 +1,241 @@
+# Error Handling Guide
+
+This guide explains how to handle errors consistently across the CoRATES application using the shared error system.
+
+## Overview
+
+CoRATES uses a centralized error system defined in `@corates/shared` that provides:
+
+- **Type-safe error codes** - String-based error codes (e.g., `PROJECT_NOT_FOUND`)
+- **Canonical error shape** - Consistent error structure across frontend and backend
+- **Domain vs Transport separation** - Clear distinction between API errors and network errors
+- **Validation error details** - Field-level error information for forms
+
+## Error Types
+
+### Domain Errors
+
+Domain errors come from the backend API and always include a `statusCode`. These represent business logic errors.
+
+```javascript
+import { createDomainError, PROJECT_ERRORS } from '@corates/shared';
+
+// Create a domain error
+const error = createDomainError(PROJECT_ERRORS.NOT_FOUND, { projectId: '123' });
+// Returns: { code: 'PROJECT_NOT_FOUND', message: 'Project not found', statusCode: 404, ... }
+```
+
+### Transport Errors
+
+Transport errors occur during network/fetch operations (frontend only). They do NOT have a `statusCode`.
+
+```javascript
+import { createTransportError } from '@corates/shared';
+
+// Create a transport error
+const error = createTransportError('TRANSPORT_NETWORK_ERROR');
+// Returns: { code: 'TRANSPORT_NETWORK_ERROR', message: 'Unable to connect...', ... }
+```
+
+### Validation Errors
+
+Validation errors are a special type of domain error with field-level details.
+
+```javascript
+import { createValidationError, createMultiFieldValidationError } from '@corates/shared';
+
+// Single field error
+const error = createValidationError('email', 'VALIDATION_FIELD_REQUIRED', '');
+// Returns: { code: 'VALIDATION_FIELD_REQUIRED', details: { field: 'email', value: '', ... }, ... }
+
+// Multi-field error
+const multiError = createMultiFieldValidationError([
+ { field: 'email', code: 'VALIDATION_FIELD_REQUIRED', message: 'Email is required' },
+ { field: 'password', code: 'VALIDATION_FIELD_TOO_SHORT', message: 'Password too short' },
+]);
+```
+
+## Backend Usage
+
+### Creating Domain Errors
+
+```javascript
+// packages/workers/src/routes/projects.js
+import { createDomainError, PROJECT_ERRORS } from '@corates/shared';
+
+export async function getProject(c) {
+ const project = await db.getProject(c.req.param('id'));
+
+ if (!project) {
+ return c.json(createDomainError(PROJECT_ERRORS.NOT_FOUND, { projectId: c.req.param('id') }), 404);
+ }
+
+ return c.json(project);
+}
+```
+
+### Validation Middleware
+
+The validation middleware automatically creates validation errors:
+
+```javascript
+// packages/workers/src/config/validation.js
+// Already configured to use shared error system
+// Returns DomainError objects with validation details
+```
+
+## Frontend Usage
+
+### Handling API Errors
+
+```javascript
+// packages/web/src/lib/error-utils.js
+import { handleFetchError, handleDomainError } from '@/lib/error-utils.js';
+
+// Wrap fetch calls
+try {
+ const response = await handleFetchError(fetch('/api/projects'), { showToast: true });
+ const data = await response.json();
+} catch (error) {
+ // Error already handled (toast shown, etc.)
+ // error is a DomainError or TransportError
+}
+```
+
+### Form Error Handling
+
+```javascript
+// packages/web/src/lib/form-errors.js
+import { createFormErrorSignals, handleFormError } from '@/lib/form-errors.js';
+import { createSignal } from 'solid-js';
+
+function MyForm() {
+ const errors = createFormErrorSignals(createSignal);
+
+ async function handleSubmit() {
+ try {
+ const response = await fetch('/api/projects', { ... });
+ // ...
+ } catch (error) {
+ // Handle validation errors with field details
+ errors.handleError(error);
+ }
+ }
+
+ return (
+
+ );
+}
+```
+
+### Error Boundaries
+
+Error boundaries catch rendering errors and unknown/programmer errors:
+
+```javascript
+// packages/web/src/components/ErrorBoundary.jsx
+import AppErrorBoundary from '@/components/ErrorBoundary.jsx';
+
+function App() {
+ return (
+
+
+
+ );
+}
+```
+
+## Error Code Reference
+
+### Authentication Errors (`AUTH_*`)
+
+- `AUTH_REQUIRED` - Authentication required (401)
+- `AUTH_INVALID` - Invalid credentials (401)
+- `AUTH_EXPIRED` - Session expired (401)
+- `AUTH_FORBIDDEN` - Access denied (403)
+
+### Validation Errors (`VALIDATION_*`)
+
+- `VALIDATION_FIELD_REQUIRED` - Required field missing (400)
+- `VALIDATION_FIELD_INVALID_FORMAT` - Invalid format (400)
+- `VALIDATION_FIELD_TOO_LONG` - Value too long (400)
+- `VALIDATION_FIELD_TOO_SHORT` - Value too short (400)
+- `VALIDATION_MULTI_FIELD` - Multiple field errors (400)
+- `VALIDATION_FAILED` - General validation failure (400)
+
+### Project Errors (`PROJECT_*`)
+
+- `PROJECT_NOT_FOUND` - Project not found (404)
+- `PROJECT_ACCESS_DENIED` - Access denied (403)
+- `PROJECT_MEMBER_EXISTS` - User already a member (409)
+- `PROJECT_LAST_OWNER` - Cannot remove last owner (400)
+
+### File Errors (`FILE_*`)
+
+- `FILE_TOO_LARGE` - File exceeds size limit (413)
+- `FILE_INVALID_TYPE` - Invalid file type (400)
+- `FILE_NOT_FOUND` - File not found (404)
+- `FILE_UPLOAD_FAILED` - Upload failed (500)
+
+### Transport Errors (`TRANSPORT_*`)
+
+- `TRANSPORT_NETWORK_ERROR` - Network connection failed
+- `TRANSPORT_TIMEOUT` - Request timed out
+- `TRANSPORT_CORS_ERROR` - CORS error
+
+### Unknown Errors (`UNKNOWN_*`)
+
+- `UNKNOWN_PROGRAMMER_ERROR` - Programmer error (500)
+- `UNKNOWN_UNHANDLED_ERROR` - Unhandled error (500)
+- `UNKNOWN_INVALID_RESPONSE` - Invalid API response (500)
+
+## Best Practices
+
+### ✅ DO
+
+- Use error helpers from `@corates/shared`
+- Handle domain errors with `handleDomainError()`
+- Handle transport errors with `handleTransportError()`
+- Use form error utilities for validation errors
+- Wrap fetch calls with `handleFetchError()`
+- Use error boundaries for rendering errors
+
+### ❌ DON'T
+
+- Throw string literals (use `no-throw-literal` ESLint rule)
+- Create raw `Error()` objects without error codes
+- Use string matching for error codes
+- Mix domain and transport error handling
+- Ignore error boundaries
+
+## ESLint Rules
+
+The project includes ESLint rules to enforce error handling:
+
+- `no-throw-literal: error` - Prevents throwing string literals
+
+## Migration Guide
+
+When migrating existing code:
+
+1. Replace numeric error codes with string codes from `@corates/shared`
+2. Replace `createErrorResponse()` with `createDomainError()`
+3. Replace string-based error matching with type-safe code checks
+4. Use `handleFetchError()` for all fetch calls
+5. Use form error utilities for form validation
+
+## Examples
+
+See the test files for comprehensive examples:
+
+- `packages/shared/src/errors/__tests__/helpers.test.ts`
+- `packages/web/src/lib/__tests__/error-utils.test.js`
+- `packages/web/src/lib/__tests__/form-errors.test.js`
diff --git a/packages/docs/guides/index.md b/packages/docs/guides/index.md
new file mode 100644
index 000000000..af33467df
--- /dev/null
+++ b/packages/docs/guides/index.md
@@ -0,0 +1,26 @@
+# Guides
+
+Practical guides for common development tasks and patterns in CoRATES.
+
+## Available Guides
+
+### Core Development
+
+- [State Management](/guides/state-management) - Managing application state with SolidJS stores
+- [Primitives](/guides/primitives) - Reusable hooks and primitives for SolidJS
+- [Components](/guides/components) - Component development patterns and best practices
+- [API Development](/guides/api-development) - Backend API route development patterns
+
+### System-Specific
+
+- [Authentication](/guides/authentication) - Authentication setup, configuration, and usage with Better Auth
+- [Yjs Sync](/guides/yjs-sync) - Collaborative editing and synchronization with Yjs
+- [Database](/guides/database) - Database schema, Drizzle ORM patterns, and migration workflow
+
+### Supporting
+
+- [Configuration](/guides/configuration) - Configuration files, environment variables, and path aliases
+- [Testing](/guides/testing) - Testing philosophy, patterns, and best practices for frontend and backend
+- [Development Workflow](/guides/development-workflow) - Getting started, development commands, and common tasks
+- [Error Handling](/guides/error-handling) - How to handle errors consistently across the application
+- [Style Guide](/guides/style-guide) - UI/UX guidelines and design system
diff --git a/packages/docs/guides/primitives.md b/packages/docs/guides/primitives.md
new file mode 100644
index 000000000..b60df1c89
--- /dev/null
+++ b/packages/docs/guides/primitives.md
@@ -0,0 +1,505 @@
+# Primitives Guide
+
+This guide explains the primitives (hooks) pattern in CoRATES, when to create primitives, and how to structure them.
+
+## Overview
+
+Primitives are reusable SolidJS hooks that encapsulate business logic, state management, and side effects. They keep components lean by moving logic out of components into reusable functions.
+
+## What Are Primitives?
+
+Primitives are similar to React hooks - they're functions that:
+
+- Start with `use` (e.g., `useProject`, `useSubscription`)
+- Use SolidJS reactive primitives (`createSignal`, `createStore`, `createMemo`, `createEffect`)
+- Return reactive values and helper functions
+- Can be composed together
+- Handle their own cleanup
+
+## When to Create a Primitive
+
+### Create a Primitive When
+
+1. **Logic is reused** - Multiple components need the same logic
+2. **Complex state/effects** - Managing connections, subscriptions, or complex state
+3. **Business logic** - Domain-specific operations (e.g., project operations, auth)
+4. **Side effects** - API calls, WebSocket connections, event listeners
+
+### Use a Component When
+
+1. **UI-only** - Pure rendering with no business logic
+2. **Component-specific** - Logic that only applies to one component
+
+### Use a Utility When
+
+1. **Pure functions** - No state or side effects
+2. **Stateless operations** - Data transformations, validations
+
+### Use a Store When
+
+1. **Global state** - Shared across many components/features
+2. **Persistent state** - Needs to survive navigation
+
+### Decision Tree
+
+```
+Is this logic reusable across multiple components?
+├─ YES → Does it manage state or side effects?
+│ ├─ YES → Create a primitive (hook)
+│ └─ NO → Create a utility function
+│
+└─ NO → Is it business logic or state management?
+ ├─ YES → Consider if it should be a store (if global) or primitive (if scoped)
+ └─ NO → Keep in component
+```
+
+## Primitive Structure
+
+### Basic Primitive Pattern
+
+```js
+import { createSignal, createEffect, onCleanup } from 'solid-js';
+
+export function useMyPrimitive(options = {}) {
+ // Internal state
+ const [value, setValue] = createSignal(null);
+ const [loading, setLoading] = createSignal(false);
+ const [error, setError] = createSignal(null);
+
+ // Side effects
+ createEffect(() => {
+ // React to changes
+ const someValue = options.someProp?.();
+ if (someValue) {
+ // Do something
+ }
+ });
+
+ // Cleanup
+ onCleanup(() => {
+ // Clean up subscriptions, timers, etc.
+ });
+
+ // Helper functions
+ async function doSomething() {
+ setLoading(true);
+ try {
+ // Perform operation
+ setValue(result);
+ } catch (err) {
+ setError(err);
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ // Return reactive values and helpers
+ return {
+ value,
+ loading,
+ error,
+ doSomething,
+ };
+}
+```
+
+### Primitive with Store Integration
+
+Primitives often interact with stores:
+
+```js
+import projectStore from '@/stores/projectStore.js';
+import projectActionsStore from '@/stores/projectActionsStore';
+
+export function useProjectData(projectId) {
+ // Read from store reactively
+ const project = () => projectStore.getProject(projectId);
+ const studies = () => projectStore.getStudies(projectId);
+ const connectionState = () => projectStore.getConnectionState(projectId);
+
+ // Helper to check if connected
+ const isConnected = () => connectionState().connected;
+
+ return {
+ project,
+ studies,
+ isConnected,
+ connectionState,
+ };
+}
+```
+
+## Primitive Examples
+
+### useProject - Complex Connection Management
+
+The `useProject` primitive manages Yjs connections, sync, and project operations:
+
+```149:160:packages/web/src/primitives/useProject/index.js
+export function useProject(projectId) {
+ const isOnline = useOnlineStatus();
+
+ // Get or create a shared connection for this project
+ const connection = getOrCreateConnection(projectId);
+
+ // Initialize connection if not already initialized
+ if (!connection.initialized) {
+ connection.initialized = true;
+
+ // Set up IndexedDB persistence
+ connection.indexeddbProvider = new IndexeddbPersistence(
+ `${INDEXEDDB_PREFIX}${projectId}`,
+ connection.ydoc,
+ );
+```
+
+This primitive:
+
+- Manages WebSocket connections
+- Handles IndexedDB persistence
+- Coordinates sync between Yjs and the store
+- Provides operations for studies, checklists, PDFs
+- Handles cleanup on disconnect
+
+### useSubscription - Resource-Based Primitive
+
+The `useSubscription` primitive uses `createResource` for async data:
+
+```55:75:packages/web/src/primitives/useSubscription.js
+export function useSubscription() {
+ const { isLoggedIn } = useBetterAuth();
+
+ // Only fetch subscription when user is logged in
+ // This prevents errors during signout when component is still mounted
+ const [subscription, { refetch, mutate }] = createResource(
+ () => (isLoggedIn() ? getSubscriptionSafe() : null),
+ {
+ initialValue: DEFAULT_SUBSCRIPTION,
+ },
+ );
+
+ /**
+ * Current subscription tier
+ */
+ const tier = createMemo(() => subscription()?.tier ?? 'free');
+
+ /**
+ * Whether the subscription is active
+ */
+```
+
+This primitive:
+
+- Uses `createResource` for async data fetching
+- Provides memoized computed values
+- Handles loading and error states
+- Returns permission helpers
+
+### useProjectData - Lightweight Store Wrapper
+
+The `useProjectData` primitive provides a lightweight way to read project data:
+
+```22:60:packages/web/src/primitives/useProjectData.js
+export function useProjectData(projectId, options = {}) {
+ const { autoConnect = true } = options;
+
+ // If autoConnect is enabled and we don't have a connection, establish one
+ // This ensures the store gets populated
+ let projectHook = null;
+ if (autoConnect) {
+ // Only create connection if we need one
+ const connectionState = () => projectStore.getConnectionState(projectId);
+ const needsConnection = () => !connectionState().connected && !connectionState().connecting;
+
+ if (needsConnection()) {
+ projectHook = useProject(projectId);
+ }
+ }
+
+ // Return reactive getters that read from the store
+ return {
+ // Data getters (reactive)
+ studies: () => projectStore.getStudies(projectId),
+ members: () => projectStore.getMembers(projectId),
+ meta: () => projectStore.getMeta(projectId),
+
+ // Connection state (reactive)
+ connected: () => projectStore.getConnectionState(projectId).connected,
+ connecting: () => projectStore.getConnectionState(projectId).connecting,
+ synced: () => projectStore.getConnectionState(projectId).synced,
+ error: () => projectStore.getConnectionState(projectId).error,
+
+ // Helpers
+ hasData: () => projectStore.hasProject(projectId),
+ getStudy: studyId => projectStore.getStudy(projectId, studyId),
+ getChecklist: (studyId, checklistId) =>
+ projectStore.getChecklist(projectId, studyId, checklistId),
+
+ // If we created a connection, expose disconnect
+ disconnect: projectHook?.disconnect,
+ };
+}
+```
+
+This primitive:
+
+- Provides reactive getters from the store
+- Optionally establishes connections
+- Keeps components simple when only reading data
+
+### useOnlineStatus - Simple Signal Primitive
+
+Simple primitives can just wrap browser APIs:
+
+```1:30:packages/web/src/primitives/useOnlineStatus.js
+import { createSignal, onMount, onCleanup } from 'solid-js';
+
+/**
+ * Hook to track online/offline status
+ * Returns a reactive signal that updates when network status changes
+ */
+export function useOnlineStatus() {
+ const [isOnline, setIsOnline] = createSignal(
+ typeof navigator !== 'undefined' ? navigator.onLine : true,
+ );
+
+ onMount(() => {
+ const handleOnline = () => setIsOnline(true);
+ const handleOffline = () => setIsOnline(false);
+
+ window.addEventListener('online', handleOnline);
+ window.addEventListener('offline', handleOffline);
+
+ onCleanup(() => {
+ window.removeEventListener('online', handleOnline);
+ window.removeEventListener('offline', handleOffline);
+ });
+ });
+
+ return isOnline;
+}
+
+export default useOnlineStatus;
+```
+
+This primitive:
+
+- Wraps browser API in a reactive signal
+- Handles event listener cleanup
+- Works in SSR contexts (checks for `navigator`)
+
+## Primitive Composition
+
+Primitives can use other primitives:
+
+```js
+import useOnlineStatus from '../useOnlineStatus.js';
+import projectStore from '@/stores/projectStore.js';
+
+export function useProject(projectId) {
+ // Use another primitive
+ const isOnline = useOnlineStatus();
+
+ // Use store
+ const connectionState = () => projectStore.getConnectionState(projectId);
+
+ // Combine primitives
+ const canSync = () => isOnline() && connectionState().connected;
+
+ return {
+ isOnline,
+ connectionState,
+ canSync,
+ };
+}
+```
+
+## Using Primitives in Components
+
+Import and use primitives directly in components:
+
+```js
+import { useProjectData } from '@/primitives/useProjectData.js';
+
+function MyComponent(props) {
+ // Use the primitive
+ const { studies, isConnected } = useProjectData(props.projectId);
+
+ return (
+
+
+ {study => }
+
+
+ );
+}
+```
+
+## Primitive Lifecycle
+
+### Initialization
+
+Primitives are called during component render:
+
+```js
+function MyComponent() {
+ // Primitive is initialized here
+ const data = useMyPrimitive();
+
+ // Use the primitive's return values
+ return
{data.value()}
;
+}
+```
+
+### Cleanup
+
+Use `onCleanup` for cleanup logic:
+
+```js
+export function useMyPrimitive() {
+ createEffect(() => {
+ const interval = setInterval(() => {
+ // Do something
+ }, 1000);
+
+ onCleanup(() => {
+ clearInterval(interval);
+ });
+ });
+}
+```
+
+### Multiple Instances
+
+Each component call creates a new primitive instance:
+
+```js
+function Component() {
+ // These are separate instances
+ const project1 = useProject('project-1');
+ const project2 = useProject('project-2');
+
+ // Each manages its own state
+}
+```
+
+## Best Practices
+
+### DO
+
+- Use `use` prefix for primitives
+- Return reactive values (signals, memos)
+- Handle cleanup with `onCleanup`
+- Compose primitives together
+- Keep primitives focused on one concern
+- Use stores for shared state, primitives for component-scoped state
+
+### DON'T
+
+- Don't call primitives conditionally
+- Don't use primitives for pure utilities
+- Don't expose raw setters from stores (use action stores)
+- Don't create primitives that duplicate store functionality
+- Don't forget cleanup for subscriptions/timers
+
+## Testing Primitives
+
+Test primitives by rendering them in a test component:
+
+```js
+import { describe, it, expect } from 'vitest';
+import { createRoot } from 'solid-js';
+import useOnlineStatus from '../useOnlineStatus';
+
+describe('useOnlineStatus', () => {
+ it('should return online status', () => {
+ let result;
+ createRoot(dispose => {
+ result = useOnlineStatus();
+ dispose();
+ });
+ expect(result()).toBe(navigator.onLine);
+ });
+});
+```
+
+## Common Patterns
+
+### Pattern: Resource Fetching
+
+```js
+import { createResource } from 'solid-js';
+
+export function useData(id) {
+ const [data, { refetch }] = createResource(
+ () => id(),
+ async id => {
+ const response = await fetch(`/api/data/${id}`);
+ return response.json();
+ },
+ );
+
+ return {
+ data,
+ loading: () => data.loading,
+ error: () => data.error,
+ refetch,
+ };
+}
+```
+
+### Pattern: Store Integration
+
+```js
+import myStore from '@/stores/myStore.js';
+
+export function useMyData(id) {
+ // Read from store reactively
+ const item = () => myStore.getItem(id());
+
+ // Memoized computed value
+ const displayName = createMemo(() => {
+ const i = item();
+ return i ? `${i.name} (${i.status})` : 'Loading...';
+ });
+
+ return {
+ item,
+ displayName,
+ };
+}
+```
+
+### Pattern: Connection Management
+
+```js
+export function useConnection(url) {
+ const [connected, setConnected] = createSignal(false);
+ let ws = null;
+
+ createEffect(() => {
+ const urlValue = url();
+ if (!urlValue) return;
+
+ ws = new WebSocket(urlValue);
+ ws.onopen = () => setConnected(true);
+ ws.onclose = () => setConnected(false);
+
+ onCleanup(() => {
+ ws?.close();
+ setConnected(false);
+ });
+ });
+
+ return {
+ connected,
+ send: data => ws?.send(JSON.stringify(data)),
+ };
+}
+```
+
+## Related Guides
+
+- [State Management Guide](/guides/state-management) - For understanding stores vs primitives
+- [Component Development Guide](/guides/components) - For using primitives in components
+- [Yjs Sync Guide](/guides/yjs-sync) - For understanding `useProject` primitive
diff --git a/packages/docs/guides/state-management.md b/packages/docs/guides/state-management.md
new file mode 100644
index 000000000..fed219571
--- /dev/null
+++ b/packages/docs/guides/state-management.md
@@ -0,0 +1,539 @@
+# State Management Guide
+
+This guide explains how state management works in CoRATES, covering the store architecture pattern, when to use stores vs props, and implementation patterns.
+
+## Overview
+
+CoRATES uses a centralized store architecture built on SolidJS's `createStore` for managing application state. The pattern separates **read operations** (data stores) from **write operations** (action stores), providing clear boundaries and eliminating prop drilling.
+
+## Store Architecture Pattern
+
+### Read Stores vs Action Stores
+
+The codebase uses a separation pattern:
+
+- **Read Stores** (`*Store.js`) - Hold cached data and provide getters/selectors
+- **Action Stores** (`*ActionsStore.js`) - Manage write operations and mutations
+
+```js
+// Read from store
+import projectStore from '@/stores/projectStore.js';
+const projects = () => projectStore.getProjectList();
+
+// Write via actions store
+import projectActionsStore from '@/stores/projectActionsStore';
+projectActionsStore.createProject({ name: 'New Project' });
+```
+
+### Key Benefits
+
+- **No prop drilling** - Components import stores directly
+- **Single source of truth** - Data lives in one place
+- **Clear separation** - Reads vs writes are explicit
+- **Reactive updates** - SolidJS store updates trigger UI re-renders
+- **Offline support** - Stores handle caching and persistence
+
+## When to Use Stores
+
+### Use Stores For
+
+1. **Shared/cross-feature state** - Data used across multiple components/features
+2. **Persistent data** - Data that should survive navigation
+3. **Cached API data** - Data fetched from APIs that should be cached
+4. **Connection state** - WebSocket/Yjs connection status
+5. **User session** - Authentication state and user data
+
+### Use Props For
+
+1. **Local component configuration** - Settings specific to one component
+2. **Parent-child communication** - Data passed directly from parent
+3. **UI state only** - Modal open/close, form field values (unless shared)
+
+### Use Context For
+
+1. **Feature-scoped state** - State that only matters within a feature tree
+2. **Avoid if possible** - Prefer stores for shared state
+
+### Decision Tree
+
+```
+Is this state shared across multiple components/features?
+├─ YES → Use a store
+│ └─ Does it need write operations?
+│ ├─ YES → Create both *Store.js and *ActionsStore.js
+│ └─ NO → Create just *Store.js
+│
+└─ NO → Is it configuration for a single component?
+ ├─ YES → Use props
+ └─ NO → Use createSignal or createStore (local state)
+```
+
+## Store Implementation Patterns
+
+### Creating a Read Store
+
+Read stores use SolidJS `createStore` for reactive state management:
+
+```js
+import { createStore, produce } from 'solid-js/store';
+
+function createMyStore() {
+ const [store, setStore] = createStore({
+ items: [],
+ loading: false,
+ error: null,
+ });
+
+ // Getters
+ function getItems() {
+ return store.items;
+ }
+
+ function getItem(id) {
+ return store.items.find(item => item.id === id);
+ }
+
+ // Setters (internal use only, prefer action stores for mutations)
+ function setItems(items) {
+ setStore('items', items);
+ }
+
+ // Complex updates using produce
+ function updateItem(id, updates) {
+ setStore(
+ produce(s => {
+ const item = s.items.find(item => item.id === id);
+ if (item) {
+ Object.assign(item, updates);
+ }
+ }),
+ );
+ }
+
+ return {
+ store, // Expose raw store for reactive access
+ getItems,
+ getItem,
+ setItems,
+ updateItem,
+ };
+}
+
+// Create singleton
+const myStore = createMyStore();
+export default myStore;
+```
+
+### Creating an Action Store
+
+Action stores manage write operations and coordinate with read stores:
+
+```js
+import myStore from '@/stores/myStore.js';
+import { handleFetchError } from '@/lib/error-utils.js';
+import { showToast } from '@corates/ui';
+import { API_BASE } from '@config/api.js';
+
+function createMyActionsStore() {
+ async function createItem(data) {
+ try {
+ const response = await handleFetchError(
+ fetch(`${API_BASE}/api/items`, {
+ method: 'POST',
+ body: JSON.stringify(data),
+ credentials: 'include',
+ }),
+ { showToast: true },
+ );
+
+ const newItem = await response.json();
+
+ // Update read store
+ const currentItems = myStore.getItems();
+ myStore.setItems([...currentItems, newItem]);
+
+ showToast.success('Item created');
+ return newItem;
+ } catch (error) {
+ // Error already handled by handleFetchError
+ throw error;
+ }
+ }
+
+ async function updateItem(id, updates) {
+ try {
+ const response = await handleFetchError(
+ fetch(`${API_BASE}/api/items/${id}`, {
+ method: 'PATCH',
+ body: JSON.stringify(updates),
+ credentials: 'include',
+ }),
+ { showToast: true },
+ );
+
+ const updatedItem = await response.json();
+
+ // Update read store
+ myStore.updateItem(id, updatedItem);
+
+ showToast.success('Item updated');
+ return updatedItem;
+ } catch (error) {
+ throw error;
+ }
+ }
+
+ return {
+ createItem,
+ updateItem,
+ };
+}
+
+const myActionsStore = createMyActionsStore();
+export default myActionsStore;
+```
+
+### Store with localStorage Caching
+
+Stores can cache data in localStorage for offline support:
+
+```88:97:packages/web/src/stores/projectStore.js
+ const [store, setStore] = createStore({
+ // Cached project data by projectId (Y.js data: studies, members, meta)
+ projects: {},
+ // Currently active project
+ activeProjectId: null,
+ // Connection states by projectId
+ connections: {},
+ // Project list from API (for dashboard)
+ projectList: initialProjectList,
+ });
+```
+
+Example caching pattern:
+
+```26:49:packages/web/src/stores/projectStore.js
+ function loadCachedProjectList() {
+ if (typeof window === 'undefined') return null;
+ try {
+ const cached = localStorage.getItem(PROJECT_LIST_CACHE_KEY);
+ const timestamp = localStorage.getItem(PROJECT_LIST_CACHE_TIMESTAMP_KEY);
+ const cachedUserId = localStorage.getItem(PROJECT_LIST_CACHE_USER_ID_KEY);
+ if (!cached || !timestamp) return null;
+
+ const age = Date.now() - parseInt(timestamp, 10);
+ if (age > PROJECT_LIST_CACHE_MAX_AGE) {
+ // Cache expired, clear it
+ localStorage.removeItem(PROJECT_LIST_CACHE_KEY);
+ localStorage.removeItem(PROJECT_LIST_CACHE_TIMESTAMP_KEY);
+ localStorage.removeItem(PROJECT_LIST_CACHE_USER_ID_KEY);
+ return null;
+ }
+
+ return { projects: JSON.parse(cached), userId: cachedUserId };
+ } catch (err) {
+ console.error('Error loading cached project list:', err);
+ return null;
+ }
+ }
+```
+
+## Using Stores in Components
+
+### Reading from Stores
+
+Import the store directly and use getters:
+
+```js
+import projectStore from '@/stores/projectStore.js';
+
+function MyComponent() {
+ // Reactive getter - updates when store changes
+ const projects = () => projectStore.getProjectList();
+
+ // Direct access to store (if needed)
+ const activeProjectId = () => projectStore.store.activeProjectId;
+
+ return (
+
+ {project => }
+
+ );
+}
+```
+
+### Writing via Action Stores
+
+Import the action store and call mutation methods:
+
+```js
+import projectActionsStore from '@/stores/projectActionsStore';
+
+function CreateProjectForm() {
+ async function handleSubmit(e) {
+ e.preventDefault();
+ const formData = new FormData(e.target);
+
+ await projectActionsStore.createProject({
+ name: formData.get('name'),
+ description: formData.get('description'),
+ });
+ // Store updates automatically, UI re-renders
+ }
+
+ return ;
+}
+```
+
+### Never Prop-Drill Store Data
+
+```js
+// WRONG - prop drilling
+function App() {
+ const projects = () => projectStore.getProjectList();
+ return ;
+}
+
+function ProjectList({ projects }) {
+ return ;
+}
+
+// CORRECT - import store directly
+function ProjectList() {
+ const projects = () => projectStore.getProjectList();
+ return ;
+}
+
+function ProjectDashboard() {
+ // Import store directly, no props needed
+ const projects = () => projectStore.getProjectList();
+ // ...
+}
+```
+
+## Store Examples
+
+### Project Store
+
+The project store manages project data, connection states, and caching:
+
+```99:112:packages/web/src/stores/projectStore.js
+ function getProject(projectId) {
+ return store.projects[projectId];
+ }
+
+ /**
+ * Get active project data
+ */
+ function getActiveProject() {
+ if (!store.activeProjectId) return null;
+ return store.projects[store.activeProjectId] || null;
+ }
+
+ /**
+ * Set the active project
+ */
+ function setActiveProject(projectId) {
+ setStore('activeProjectId', projectId);
+ }
+```
+
+### Project Actions Store
+
+The actions store manages all write operations:
+
+```26:67:packages/web/src/stores/projectActionsStore/index.js
+function createProjectActionsStore() {
+ /**
+ * Map of projectId -> Y.js connection operations
+ * Set by useProject hook when connecting
+ * @type {Map}
+ */
+ const connections = new Map();
+
+ /**
+ * The currently active project ID.
+ * Set by ProjectView when a project is opened.
+ * Most methods use this automatically so components don't need to pass it.
+ */
+ let activeProjectId = null;
+
+ // ============================================================================
+ // Internal: Active Project & User Access
+ // ============================================================================
+
+ /**
+ * Set the active project (called by ProjectView on mount)
+ */
+ function _setActiveProject(projectId) {
+ activeProjectId = projectId;
+ }
+
+ /**
+ * Clear the active project (called by ProjectView on unmount)
+ */
+ function _clearActiveProject() {
+ activeProjectId = null;
+ }
+
+ /**
+ * Get the active project ID, throws if none set
+ */
+ function getActiveProjectId() {
+ if (!activeProjectId) {
+ throw new Error('No active project - are you inside a ProjectView?');
+ }
+ return activeProjectId;
+ }
+
+ /**
+ * Get active project ID or null (for components that just need to check)
+ */
+ function getActiveProjectIdOrNull() {
+ return activeProjectId;
+ }
+
+ /**
+ * Get current user ID from auth store
+ */
+ function getCurrentUserId() {
+ const auth = useBetterAuth();
+ return auth.user()?.id || null;
+ }
+```
+
+### Better Auth Store
+
+The auth store wraps Better Auth with caching and offline support:
+
+```18:65:packages/web/src/api/better-auth-store.js
+function createBetterAuthStore() {
+ // Track online status without reactive primitives (for singleton context)
+ const [isOnline, setIsOnline] = createSignal(navigator.onLine);
+
+ // Listen for online/offline events
+ if (typeof window !== 'undefined') {
+ window.addEventListener('online', () => setIsOnline(true));
+ window.addEventListener('offline', () => setIsOnline(false));
+ }
+
+ function loadCachedAuth() {
+ if (typeof window === 'undefined') return null;
+ try {
+ const cached = localStorage.getItem(AUTH_CACHE_KEY);
+ const timestamp = localStorage.getItem(AUTH_CACHE_TIMESTAMP_KEY);
+ if (!cached || !timestamp) return null;
+
+ const age = Date.now() - parseInt(timestamp, 10);
+ if (age > AUTH_CACHE_MAX_AGE) {
+ localStorage.removeItem(AUTH_CACHE_KEY);
+ localStorage.removeItem(AUTH_CACHE_TIMESTAMP_KEY);
+ return null;
+ }
+
+ return JSON.parse(cached);
+ } catch (err) {
+ console.error('Error loading cached auth:', err);
+ return null;
+ }
+ }
+
+ // Save auth data to localStorage
+ function saveCachedAuth(userData) {
+ if (typeof window === 'undefined') return;
+ try {
+ if (userData) {
+ localStorage.setItem(AUTH_CACHE_KEY, JSON.stringify(userData));
+ localStorage.setItem(AUTH_CACHE_TIMESTAMP_KEY, Date.now().toString());
+ } else {
+ localStorage.removeItem(AUTH_CACHE_KEY);
+ localStorage.removeItem(AUTH_CACHE_TIMESTAMP_KEY);
+ }
+ } catch (err) {
+ console.error('Error saving cached auth:', err);
+ }
+ }
+```
+
+## Store Lifecycle and Cleanup
+
+### Initialization
+
+Stores are singletons created at module load time:
+
+```604:607:packages/web/src/stores/projectStore.js
+// Create singleton store without createRoot
+// createStore doesn't need a reactive owner/root context
+const projectStore = createProjectStore();
+
+export default projectStore;
+```
+
+### Cache Validation
+
+Stores should validate cached data when appropriate:
+
+```498:534:packages/web/src/stores/projectStore.js
+ function validateProjectListCache(currentUserId) {
+ if (!currentUserId) {
+ // No user ID, clear the cache
+ setStore('projectList', {
+ items: [],
+ loaded: false,
+ loading: false,
+ error: null,
+ cachedUserId: null,
+ });
+ // Clear localStorage cache
+ localStorage.removeItem(PROJECT_LIST_CACHE_KEY);
+ localStorage.removeItem(PROJECT_LIST_CACHE_TIMESTAMP_KEY);
+ localStorage.removeItem(PROJECT_LIST_CACHE_USER_ID_KEY);
+ return;
+ }
+
+ const cachedUserId = store.projectList.cachedUserId;
+
+ // If cached user ID doesn't match current user, clear the cache
+ if (cachedUserId && cachedUserId !== currentUserId) {
+ console.log(
+ '[projectStore] Cached project list belongs to different user, clearing cache',
+ );
+ setStore('projectList', {
+ items: [],
+ loaded: false,
+ loading: false,
+ error: null,
+ cachedUserId: null,
+ });
+ // Clear localStorage cache
+ localStorage.removeItem(PROJECT_LIST_CACHE_KEY);
+ localStorage.removeItem(PROJECT_LIST_CACHE_TIMESTAMP_KEY);
+ localStorage.removeItem(PROJECT_LIST_CACHE_USER_ID_KEY);
+ }
+ }
+```
+
+## Best Practices
+
+### DO
+
+- Separate read stores from action stores
+- Use `produce` for complex nested updates
+- Cache data in localStorage for offline support
+- Validate cached data (expiry, user matching, etc.)
+- Use getters/selectors instead of exposing raw store
+- Import stores directly in components (no prop drilling)
+
+### DON'T
+
+- Don't prop-drill store data
+- Don't mutate store directly (use setters or action stores)
+- Don't expose raw `setStore` from read stores
+- Don't create stores for local component state
+- Don't forget to handle cache invalidation
+
+## Related Guides
+
+- [Primitives Guide](/guides/primitives) - For store-like patterns that are component-scoped
+- [Component Development Guide](/guides/components) - For component state patterns
+- [Yjs Sync Guide](/guides/yjs-sync) - For understanding how Yjs updates stores
diff --git a/packages/docs/guides/style-guide.md b/packages/docs/guides/style-guide.md
new file mode 100644
index 000000000..0ba4682d8
--- /dev/null
+++ b/packages/docs/guides/style-guide.md
@@ -0,0 +1,402 @@
+# CoRATES UI Style Guide
+
+## Brand Identity
+
+### Application Name
+
+**CoRATES** - Collaborative Research Appraisal Tool for Evidence Synthesis
+
+### Logo/Brand Icon
+
+- Circular checkmark icon
+- Primary color: `blue-600`
+- White checkmark symbol inside
+
+## Color Palette
+
+### Primary Colors
+
+- **Blue Primary**: `blue-600` (#2563eb)
+- **Blue Dark**: `blue-700` (#1d4ed8)
+- **Blue Light**: `blue-500` (#3b82f6)
+- **Blue Hover**: `blue-800` (#1e40af)
+
+### Accent Colors
+
+- **Red**: `red-600` (#dc2626) for destructive actions
+- **Red Hover**: `red-700` (#b91c1c)
+- **Green**: For success states (implied from traffic light patterns)
+
+### Neutral Colors
+
+- **Gray Scale**:
+ - `gray-50` (#f9fafb) - lightest backgrounds
+ - `gray-100` (#f3f4f6) - subtle backgrounds
+ - `gray-200` (#e5e7eb) - borders, dividers
+ - `gray-300` (#d1d5db) - disabled states
+ - `gray-400` (#9ca3af) - placeholder text, icons
+ - `gray-500` (#6b7280) - secondary text
+ - `gray-600` (#4b5563) - body text
+ - `gray-700` (#374151) - headings
+ - `gray-800` (#1f2937) - dark text
+ - `gray-900` (#111827) - darkest text
+
+### Background Colors
+
+- **Main Background**: `bg-blue-50`
+- **Card Background**: `bg-white`
+- **Feature Backgrounds**: `bg-blue-50`, `bg-blue-100`
+- **Sidebar Background**: Light gray tones
+
+## Typography
+
+### Font Family
+
+Primary: `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif`
+
+### Font Sizes
+
+- **4XS**: `0.375rem` (6px)
+- **3XS**: `0.5rem` (8px)
+- **2XS**: `0.625rem` (10px)
+- **XS**: `text-xs` - 12px
+- **SM**: `text-sm` - 14px
+- **Base**: `text-base` - 16px
+- **LG**: `text-lg` - 18px
+- **XL**: `text-xl` - 20px
+- **2XL**: `text-2xl` - 24px
+- **3XL**: `text-3xl` - 30px
+- **4XL**: `text-4xl` - 36px
+- **6XL**: `text-6xl` - 60px
+- **7XL**: `text-7xl` - 72px
+
+### Font Weights
+
+- **Medium**: `font-medium`
+- **Semibold**: `font-semibold`
+- **Bold**: `font-bold`
+- **Extrabold**: `font-extrabold` (for brand name)
+
+### Text Colors
+
+- **Primary**: `text-gray-900`
+- **Secondary**: `text-gray-600`
+- **Muted**: `text-gray-500`
+- **Placeholder**: `text-gray-400`
+- **Brand**: `text-blue-600`
+- **Links**: `text-blue-600`
+
+## Layout & Spacing
+
+### Container Patterns
+
+- **Max Width**: `max-w-4xl mx-auto` for main content
+- **Page Padding**: `p-6` for main content areas
+- **Card Padding**: `p-4` to `p-8` depending on component
+
+### Spacing Scale
+
+- **Gap/Margin**: `gap-2`, `gap-4`, `gap-6`, `gap-8`
+- **Padding**: `p-2`, `p-4`, `p-6`, `p-8`, `p-12`
+
+### Breakpoints
+
+- **XS**: `30rem` (480px) - Custom breakpoint
+- **SM**: Standard Tailwind breakpoints apply
+
+## Components
+
+### Buttons
+
+#### Primary Button
+
+```jsx
+