22 KiB
Project Guidelines
Protecting This File
IMPORTANT: This file (claude.md) is gitignored and can be lost during branch operations!
Git doesn't protect untracked/ignored files during git checkout or branch switches. Before major git operations:
cp claude.md claude.md.backup
Coding Best Practices (CRITICAL)
Read coding-best-practices.md before writing any code. It contains personal coding conventions that apply to every project — naming rules, file/function size limits, folder nesting patterns, persistence rules, and language-specific structure guides. Those rules override defaults and must be followed.
File Header Check Rule
Every time you open a file to work on it, check the file header first. Look for outstanding @todo items, unresolved @issues, or outdated @status notes. If any are completed or resolved, clean them up before doing other work. If a TODO is relevant to the current task, address it. This ensures nothing falls through the cracks.
Project Structure (CRITICAL)
This project follows a standardized structure for future modularity. The long-term goal is to enable drag-and-drop of components between projects. This structure must be followed.
ProjectRoot/ # git root
├── claude.md # (gitignored) AI assistant guidelines
├── coding-best-practices.md # Coding conventions (read before Phase 1)
├── build_spec.md # (gitignored) Full project specification
├── build_plan.md # (gitignored) Phased task checklist
├── .gitignore
├── docs/ # Public-facing documentation (parsed to HTML for website)
│ ├── README.md # Docs index with table of contents
│ ├── getting-started.md
│ ├── architecture.md
│ ├── development.md
│ ├── deployment.md
│ └── feature-*.md # Feature docs (added per phase)
├── InternalDocs/ # Internal development documentation (not public)
│ └── ... # Design decisions, technical debt, internal API docs
└── app/ # ALL project code lives here
├── src/
│ ├── assets/ # Static assets
│ ├── boot/ # Quasar boot files (nhost init, etc.)
│ ├── components/ # Vue components
│ ├── composables/ # Vue composables
│ ├── layouts/ # Page layouts
│ ├── pages/ # Route pages
│ ├── router/ # Vue Router config
│ ├── stores/ # Pinia stores
│ ├── services/ # API services
│ └── types/ # TypeScript types
├── nhost/ # Nhost configuration
│ ├── functions/ # Serverless functions
│ ├── migrations/ # Database migrations
│ └── metadata/ # Hasura metadata
├── public/ # Public static files
├── docker-compose.yml
├── .env # Dev environment (gitignored)
└── .env.prod # Prod environment (gitignored)
Terminology
- "build spec" or "spec" refers to
build_spec.md- the full project description - "build plan" or "plan" refers to
build_plan.md- the phased task checklist
Onboarding Questions (Ask at Project Start)
At the start of every new project conversation, gather these details before writing any code:
- Brand colors & fonts - Primary, secondary, accent colors. Typography preferences. Any existing brand guidelines or color palette?
- App personality & tone - Formal or casual? Dark or light default theme? Dense or spacious layout? Any design inspiration or reference apps?
- Target platform - Desktop-first, mobile-first, or responsive? Specific OS targets (macOS, Windows, Linux)? Minimum screen sizes?
- Auth requirements - Login method (email/password, OAuth, magic link)? User roles and permissions? Public or private app?
Don't assume defaults for any of these. Ask, get answers, then document them in the Project-Specific Configuration section at the bottom of this file.
Tech Stack
- Frontend: Quasar Framework (Vue 3) - SPA mode
- Desktop (optional): Tauri (Rust) - Cross-platform builds (macOS, Windows, Linux)
- Backend: Nhost (self-hosted via Docker)
- PostgreSQL - Database
- Hasura - GraphQL API
- Auth - Authentication service
- Storage - File storage
- Functions - Serverless functions (Node.js)
- Containerization: Docker / Docker Compose
Tauri Desktop Apps
When building desktop apps with Tauri:
- Add
src-tauri/folder insideapp/ - Use the build-and-run approach (no hot reload) for consistent dev/prod builds
- Separate bundle IDs for dev and prod (can run both simultaneously)
Testing Changes (CRITICAL)
After every code editing session, you MUST rebuild the app so the user can test your changes.
cd app
./build.sh -dev -open
Why This Matters
- This uses a build-and-run approach, not hot reload
- Your code changes are NOT visible until you rebuild
- Always rebuild before asking the user to test
- If the user reports issues, check the logs first:
./build.sh logs
Build Script
Always use build.sh for building the app. This ensures consistent builds and logging for collaborative debugging.
cd app
./build.sh -dev -open # Build dev and open app
./build.sh -prod # Build prod to build/prod/
./build.sh -install # Build prod and open Finder for drag-to-Applications
./build.sh logs # View latest console output (last 100 lines)
./build.sh clean # Clean build artifacts (preserves logs)
./build.sh clean-logs # Remove all logs
Commands
| Command | Description |
|---|---|
-dev |
Development build. Bundle ID: com.[app].dev |
-prod |
Production build. Builds to build/prod/. |
-install |
Build prod and open Finder for drag-to-Applications. |
-open |
Open the app after building (use with -dev or -prod) |
logs |
Show last 100 lines of console output |
clean |
Remove build artifacts (preserves logs) |
clean-logs |
Remove all log files |
Why Build-and-Run (No Hot Reload)
This project uses a build-and-run approach instead of hot reload:
| Aspect | Hot Reload | Build-and-Run |
|---|---|---|
| Speed | Instant updates | Full rebuild required |
| Dev/Prod parity | Different environments | Identical to production |
| Debugging | Dev-only issues possible | What you test = what ships |
| Logging | Console only | Full logs to files |
The tradeoff is slower iteration, but you always test exactly what will ship.
What the Build Does
- Build with Tauri -
npx tauri builddoes all of the following:- Build frontend (
npm run build→dist/spa/) - Build Rust binary with embedded frontend
- Create .app bundle in
src-tauri/target/release/bundle/macos/
- Build frontend (
- Copy to build folder - Copies bundle to
build/dev/orbuild/prod/ - Update Info.plist - Sets correct bundle ID for dev/prod
- Install (prod only) - Copies to
~/Applications
Important: Using npx tauri build ensures the frontend is properly embedded in the binary. Manual cargo build --release does NOT embed the frontend.
Build Output
Builds are organized in separate folders:
app/build/
├── dev/
│ └── AppName.app # Dev build (bundle ID: .dev)
├── prod/
│ └── AppName.app # Prod build
├── logs/
│ ├── build_dev_*.log # Build output logs
│ └── build_latest.log # Symlink to latest
└── console/
├── console_*.log # App runtime console output
└── console_latest.log # Symlink to latest
Dev and prod have different bundle IDs, so both can run simultaneously.
Logging
All build output and app console output is logged:
- Build logs:
build/logs/build_latest.log- compilation output, errors - Console logs:
build/console/console_latest.log- runtimeconsole.log(), errors
When debugging, run ./build.sh logs to see the latest console output.
Console Logging Setup (Tauri)
For frontend console.log() to appear in the logs, you need:
- Boot file - Copy
templates/scripts/console-logger.tstoapp/src/boot/ - Register boot - Add
'console-logger'tobootarray inquasar.config.js - Rust command - Add
log_to_stdoutcommand tosrc-tauri/src/commands.rs - Cargo dep - Add
chrono = "0.4"toCargo.toml
The console-logger.ts template includes the Rust code you need to add.
How it works:
console.log() → console-logger.ts → invoke('log_to_stdout') → Rust println!() → stdout → build.sh logs
When to Clean
Run ./build.sh clean when:
- Switching between major dependency versions
- Build errors reference stale/cached files
- Rust compilation issues after Tauri config changes
- Before creating a fresh production build
Node.js Environment
Node.js version: Always match Debian stable (currently Node.js 18.x for Debian 12 Bookworm)
Check current Debian stable Node version: https://packages.debian.org/stable/nodejs
Package manager: Use npm. Commit lock files for reproducible builds.
Rules:
- Always commit
package-lock.json- required for deterministic installs - Never ignore lock files (
package-lock.json,yarn.lock,pnpm-lock.yaml) - Use
npm installto install dependencies - Use
npm run <script>to run package scripts
Communication Rules
Always explain before acting. Before running any command, SSH, database query, or making changes:
- Give a short, concise explanation of WHAT you're about to do and WHY
- Wait for approval on anything that modifies production data or server state
- Don't chain multiple server commands without explaining each step
Database Rules (CRITICAL)
NEVER make direct database changes via Hasura MCP or Hasura Console.
All database changes MUST be done through migrations so they:
- Are tracked in version control
- Persist across restarts (
nhost down/nhost up) - Can be reproduced in production
Hasura MCP Usage
- READ-ONLY: Use Hasura MCP only to inspect schema, check what tables exist, verify tracking status
- NO MUTATIONS: Never use it to create tables, add columns, track tables, or modify permissions
- MIGRATIONS ONLY: All schema changes go in
app/nhost/migrations/default/ - METADATA ONLY: All Hasura config (tracking, permissions, relationships) go in
app/nhost/metadata/
How to Make Database Changes
- Write SQL migration in
app/nhost/migrations/default/<N>_<name>/up.sql(N = next version number) - Write rollback in
app/nhost/migrations/default/<N>_<name>/down.sql - Add Hasura metadata YAML in
app/nhost/metadata/databases/default/tables/ - Restart dev environment to apply migrations
- Commit and push
Migration folder naming:
app/nhost/migrations/default/
├── 1_init_schema/
├── 2_add_users/
├── 3_add_settings/
└── ...next version number...
Hasura Role Permissions (CRITICAL)
NEVER define permissions for the admin role in Hasura metadata.
Hasura reserves admin as a special role with full unrestricted access. Defining explicit permissions for admin causes metadata errors:
"cannot define permission for admin role"
Correct approach:
- Define permissions only for
user,public, and custom roles - Users with
X-Hasura-Role: adminautomatically bypass all permission checks
Example - WRONG:
select_permissions:
- role: admin # DON'T DO THIS
permission:
columns: [id, name]
filter: {}
Example - CORRECT:
select_permissions:
- role: user # Only define for non-admin roles
permission:
columns: [id, name]
filter: { created_by: { _eq: X-Hasura-User-Id } }
Development Approach
Integrate-as-you-go - Connect real backend services as you build. Don't build elaborate mock systems. The Docker infrastructure provides:
- Database & Hasura - Real tables and GraphQL
- Nhost Auth - Real authentication
- Real data from the start
Persistence Rules (CRITICAL)
All persistence requires an explicit user action. No exceptions.
Never auto-save. Never sync state in the background. Never write to the database on a timer or watcher. Every single write to a database, file, or storage must be traceable to a deliberate user action.
The Rule
Data is only persisted when the user deliberately triggers it — clicking "Save", submitting a form, confirming a dialog, toggling a setting, etc.
WRONG — Never Do This
- Auto-saving form fields as the user types
- Syncing component state to the database on mount/unmount
- Watchers that write to storage when reactive data changes
- Debounced save on input change
- Persisting UI state automatically (sidebar position, tab selection, scroll position)
- Writing to the database during navigation or route changes
- Background sync intervals or polling that writes data
RIGHT — Always Do This
- User clicks "Save" → write to database
- User submits a form → persist the data
- User explicitly toggles a setting → save that setting
- User clicks "Delete" → confirm, then remove from database
- User drags an item to reorder → save on drop (explicit action)
Why This Matters
Auto-save creates unpredictable behavior, race conditions, and persists data the user didn't intend to save. Every write should be traceable to a conscious user decision. If you can't point to the button or action the user took to trigger the save, it shouldn't be saving.
Implementation Pattern
User Action → Service Call → Database Write → Confirm Success to User
- User triggers action (click, submit, confirm)
- Call the appropriate service/API
- Write to database
- Show success feedback (Quasar Notify or UI update)
- If it fails, show an error — don't silently retry
Code Quality
TypeScript
- Use TypeScript strict mode
- Define interfaces/types for all data structures in
src/types/ - No
anytypes unless absolutely unavoidable — useunknownand narrow instead - Type all function parameters and return values
Vue / Quasar Conventions
- Use Composition API with
<script setup lang="ts">for all components - One component per file, named in PascalCase (e.g.,
UserProfile.vue) - Composables go in
src/composables/, prefixed withuse(e.g.,useAuth.ts) - Keep components focused — if it's doing too much, split it
- Props down, events up — don't mutate props, emit events to parent
Naming
- Files: PascalCase for components (
UserCard.vue), camelCase for everything else (userService.ts) - Variables/functions: camelCase
- Constants: UPPER_SNAKE_CASE for true constants, camelCase for derived/computed values
- Types/Interfaces: PascalCase, no
Iprefix (useUsernotIUser) - CSS classes: kebab-case
General
- No magic numbers or strings — use named constants
- Keep functions short and single-purpose
- Colocate related code — don't scatter logic across files unnecessarily
- Delete dead code, don't comment it out
Error Handling
User-Facing Errors
- Always show human-readable messages — never expose raw error objects, stack traces, or technical details to the user
- Use Quasar's
Notifyplugin for transient feedback (success, warning, error) - Use inline error messages for form validation
Technical Errors
- Log technical details to console for debugging (
console.errorwith context) - Include the operation that failed and relevant IDs in log messages
- Never swallow errors silently — if you catch it, either handle it or log it
Pattern
try {
const result = await userService.updateProfile(data)
Notify.create({ type: 'positive', message: 'Profile updated' })
} catch (error) {
console.error('Failed to update profile:', error)
Notify.create({ type: 'negative', message: 'Could not update profile. Try again.' })
}
Where to Catch
- Catch at service boundaries (API calls, database operations), not deep in utility functions
- Let errors propagate up to where they can be meaningfully handled
- Don't wrap every line in try/catch — catch at the level where you can show the user feedback
Documentation
docs/ vs InternalDocs/
This project has two documentation directories with different audiences:
| Directory | Audience | Purpose | Parsed for website? |
|---|---|---|---|
docs/ |
Public | User-facing app documentation — getting started, features, architecture | Yes |
InternalDocs/ |
Developers | Internal dev context — design decisions, technical debt, internal API contracts, Claude context | No |
docs/ contains ONLY what the public needs. It gets parsed into HTML and served on the app website. No internal implementation details, no dev-only context.
InternalDocs/ contains everything internal — design rationale, known technical debt, internal API documentation, integration notes, debugging guides, anything that helps the development team but doesn't belong in public docs.
Both directories are committed to git.
Phase Documentation
Every phase ends with documentation. After completing a feature or phase:
- Create feature docs - Copy
docs/TEMPLATE-feature.mdtodocs/feature-[name].md - Update the index - Add links to
docs/README.mdtable of contents - Update core docs - Keep
architecture.md,development.mdcurrent - Update internal docs - Add design decisions, technical debt notes, or internal API docs to
InternalDocs/as needed
docs/ Structure
| File | Purpose |
|---|---|
docs/README.md |
Index with table of contents |
docs/getting-started.md |
Installation & setup |
docs/architecture.md |
Tech stack & design decisions |
docs/development.md |
Development workflow |
docs/deployment.md |
Build & deployment |
docs/feature-*.md |
Feature-specific documentation |
Naming Convention
feature-[name].md- Feature docs (e.g.,feature-authentication.md)component-[name].md- Component docsintegration-[name].md- Integration docsguide-[topic].md- How-to guides
Commit Rules
- Never mention Claude, AI, or "Generated with Claude Code" in commits
- All commits are authored by the developer - no co-author tags
- Write commit messages in first person as the developer
- Keep commit messages concise and descriptive of the changes
Branch Strategy
- main - Production branch
- test - Staging/testing branch
Workflow:
- Develop and test locally
- Push to
testbranch for staging - Verify on staging
- Merge
test→mainfor production
Environment Variables
# Nhost
NHOST_ADMIN_SECRET=
# PostgreSQL
POSTGRES_PASSWORD=
# JWT
JWT_SECRET=
# App
APP_URL=
Project-Specific Configuration
Project Name
[PROJECT_NAME]
Description
[Brief description from build_spec.md]
Database Schema
[Tables and relationships]
URLs
| Service | Local | Production |
|---|---|---|
| App | http://localhost:[dynamic] | |
| GraphQL | http://localhost:8080 |
Note: For Tauri desktop apps, use dynamic port allocation. The dev server will find the first available port.
MCP Servers
[Configure in .mcp.json if needed]
Servers
[Production server details if applicable]
Dockerized Frontend for Production
When deploying a fully dockerized project, add a frontend service to docker-compose:
1. Create app/Dockerfile.frontend
# Build stage
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# Production stage
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
2. Create app/nginx.conf
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
3. Add frontend service to docker-compose.yml
All Traefik hostnames use hardcoded localhost values. TeamIDE's proxy rewrites the Host header to localhost, so this works both locally and in production.
frontend:
build:
context: .
dockerfile: Dockerfile.frontend
depends_on:
graphql:
condition: service_healthy
auth:
condition: service_healthy
labels:
traefik.enable: "true"
traefik.http.routers.frontend.rule: Host(`localhost`)
traefik.http.routers.frontend.entrypoints: web
traefik.http.services.frontend.loadbalancer.server.port: "80"
extra_hosts:
- host.docker.internal:host-gateway
- localhost:host-gateway
- auth.localhost:host-gateway
- hasura.localhost:host-gateway
- storage.localhost:host-gateway
restart: always
4. Traefik hostname pattern
All services use hardcoded localhost hostnames:
- Frontend:
Host(\localhost`)` - Auth:
Host(\auth.localhost`)` - Hasura:
Host(\hasura.localhost`)` - Storage:
Host(\storage.localhost`)` - Functions:
Host(\functions.localhost`)`
TeamIDE Hosting
This project is deployed via TeamIDE.
- Workspace ID:
your-workspace-id-here - Use
teamide_deployto deploy changes - Use
teamide_set_env_varsto configure environment variables - Use
teamide_get_logsto check build status
To get your workspace ID, run:
teamide workspace list