Welcome to Tada development! This guide covers everything you need to build, test, and contribute to Tada.
Quick Links:
- 🏗️ Project Structure — Codebase organization
- 🎯 Design Philosophy — Vision and principles
- 📊 Entry Ontology — Classification system
- 🗺️ Roadmap — Version planning
- 📝 Changelog — Release history
- 🤖 Agent Instructions — AI-assisted development
- Bun (package manager and runtime) — Installed in dev container, or install locally
- Git — For version control
- VS Code (recommended) — With Dev Containers extension
git clone https://github.com/InfantLab/tada.git
cd tadaOption 1: Dev Container (Recommended)
- Open the project in VS Code
- Install the "Dev Containers" extension if not already installed
- Click "Reopen in Container" when prompted
- Container setup runs automatically:
- Dependencies installed via
bun install - CA certificates configured for HTTPS operations
- Git SSH agent forwarding enabled (VS Code native)
- GitHub push via HTTPS works automatically (forwarded to SSH)
- Dependencies installed via
- Start development:
cd app bun run dev
Git & GitHub in Dev Container
Git operations (push, pull, clone) work frictionlessly:
- SSH Agent Forwarding — VS Code automatically forwards your host's SSH agent to the container, so
git pushto GitHub works without additional setup - HTTPS Fallback — Even HTTPS
gitURLs are automatically redirected to SSH for added reliability - CA Certificates — Both the dev container and production image include CA certificates for HTTPS operations
- No Manual Config Needed — Just
git push, it works
Option 2: Local Development
cd app
bun install
bun run devOption 3: Docker
# Production mode
docker compose up -d
# Development mode with hot reload
docker compose --profile dev up tada-devThe app runs on http://localhost:3000
Tada uses a unified Entry model where everything is an entry:
Type (behavior) → Category (domain) → Subcategory (specific)
↓ ↓ ↓
"timed" "mindfulness" "sitting"
"tada" "accomplishment" "work"
"journal" "journal" "dream"
- Types define behavior (how it's recorded): timed activities, instant captures, journal entries
- Categories enable grouping (life domains): mindfulness, movement, creative, learning, journal, accomplishment, events
- Subcategories provide specificity: sitting meditation, running, piano practice, dream, work accomplishment
No separate tables for different activity types — just one entries table with flexible classification. Add new types/categories/subcategories without schema changes.
Read more: design/ontology.md and docs/PROJECT_STRUCTURE.md
# Development
bun run dev # Start dev server with hot reload (:3000)
# Building
bun run build # Production build
bun run preview # Preview production build locally
# Code Quality
bun run lint # Check code style with ESLint
bun run lint:fix # Auto-fix linting issues
bun run typecheck # Verify TypeScript compilation
# Database
bun run db:generate # Generate migrations from schema changes
bun run db:migrate # Apply pending migrations
bun run db:studio # Open Drizzle Studio UI (:4983)
# Testing
bun run test # Run all tests
bun run test:ui # Visual test UI
bun run test:coverage # Generate coverage reportSee dev/TESTING.md for complete testing documentation.
See PROJECT_STRUCTURE.md for complete codebase organization.
Quick reference:
app/pages/— Vue pages (file-based routing)app/components/— Reusable Vue componentsapp/server/api/— REST API endpointsapp/server/db/— Database schema and migrationsapp/utils/categoryDefaults.ts— Entry ontology configurationdesign/— Design documents (SDR, philosophy, ontology, roadmap)docs/— Developer documentation
- Framework: Nuxt 3.15.1 + Vue 3
- Language: TypeScript (strict mode enabled)
- Database: SQLite via Drizzle ORM
- Authentication: Lucia Auth v3
- PWA: @vite-pwa/nuxt
- Styling: Tailwind CSS
- Runtime: Bun 1.3.5 (not Node.js!)
- Voice Input: Web Speech API, MediaRecorder API, Web Crypto API
Core concept: Everything is an Entry — meditations, dreams, tadas, journal notes, workouts, etc.
interface Entry {
id: string; // nanoid
userId: string; // FK to users
type: string; // "timed", "tada", "journal"
category: string; // "mindfulness", "accomplishment", "journal"
subcategory: string; // "sitting", "work", "dream"
emoji: string; // Custom or default emoji
name: string; // Display name
timestamp: string; // When it occurred
durationSeconds: number | null;
notes: string | null;
data: object; // Type-specific metadata (JSON)
tags: string[]; // Searchable tags
}Key insight: Rhythms are NOT separate records — they're aggregation queries over entries using matchers.
See design/ontology.md for the three-level classification system.
- Engine: SQLite (file-based, perfect for self-hosting)
- ORM: Drizzle with full TypeScript types
- Location:
app/data/db.sqlite(auto-created, gitignored) - Schema:
app/server/db/schema.ts
Tables:
users— User accounts (Lucia Auth)sessions— Authentication sessionsentries— Unified entry model with ontology fieldsrhythms— Rhythm definitions and matcherstimer_presets— Saved timer configurationscategory_settings— User category customization (v0.2.0)attachments— Entry attachments (v0.2.0)
Migrations: Managed by Drizzle Kit. Always commit generated migration files.
We use conventional commits for clear, semantic history:
feat: add emoji picker component
fix: correct timer countdown calculation
docs: update README with ontology section
refactor: extract category logic to utils
test: add unit tests for getEntryDisplayProps
chore: upgrade emoji-picker-element to v1.28Types:
feat:— New featurefix:— Bug fixdocs:— Documentation onlyrefactor:— Code restructure (no behavior change)test:— Add/update testschore:— Maintenance (deps, config, etc.)
Branch Strategy:
main— Always deployable, protectedfeature/description— Human-authored featurescopilot/description— AI agent-authored changes
Pull Request Process:
- Create feature branch from
main - Make changes with conventional commits
- Run
bun run lint:fixandbun run typecheck - Push and open PR (CI runs automatically)
- Get code review (or self-review for small changes)
- Merge to
main(squash merge preferred)
Current State (v0.5.0):
- ✅ 80 tests passing
- ✅ Co-located tests - next to source files
⚠️ Integration tests - 4 files disabled (*.test.ts.skip) pending @nuxt/test-utils rewrite- ✅ See app/tests/README.md for complete guide
Test Structure:
app/
├── server/api/
│ ├── entries/
│ │ ├── index.get.ts
│ │ └── index.get.test.ts # API tests
sessions— Lucia auth sessionsentries— Main data table (all activities)rhythms— Rhythm definitions (query patterns)tags— Optional tagging system
- Offline-first via service worker (Workbox)
- Caching strategy:
- Cache-first: Static assets (JS, CSS, images, audio)
- Network-first: API calls
- IndexedDB: Offline data storage (via Dexie.js)
- Manifest: Defined in
nuxt.config.ts - Icons: Located in
public/icons/ - Install to home screen: Gives app-like experience
iOS Safari limits background execution for PWAs. Our solution:
- Web Worker for timing (survives tab backgrounding on Android/desktop)
- Save timer state to IndexedDB every second
- Resume gracefully if app was killed
- Push notification on completion (iOS 16.4+ when PWA installed)
Voice input allows users to capture tadas and journals by speaking naturally. The system extracts structured data from transcribed speech.
Architecture:
User speaks → MediaRecorder (WebM/mp4) → Transcription → LLM Extraction → Entry
↓
Web Speech API (free)
OR
Whisper Cloud (BYOK)
Key Components:
app/components/voice/VoiceRecorder.vue— Main recording UI with mic buttonapp/composables/useVoiceCapture.ts— MediaRecorder abstractionapp/composables/useTranscription.ts— STT with tiered fallbackapp/composables/useVoiceQueue.ts— IndexedDB queue for offline resilienceapp/utils/tadaExtractor.ts— Rule-based and LLM extraction logic
Transcription Tiers:
- Web Speech API (free, on-device where available)
- Whisper Cloud via Groq/OpenAI (BYOK - bring your own key)
API Endpoints:
POST /api/voice/transcribe— Audio to text (rate limited: 1/10s, 50/month free)POST /api/voice/structure— Text to structured entry via LLMPOST /api/voice/validate-key— Validate user's API keyGET /api/voice/usage— Usage statistics and billing period
Security:
- API keys encrypted client-side with AES-GCM before localStorage
- PBKDF2 key derivation with 100k iterations
- Keys never sent to our servers (BYOK model)
Browser Support:
| Browser | MediaRecorder | Web Speech API | Status |
|---|---|---|---|
| Chrome | ✅ webm/opus | ✅ | Full support |
| Safari | ✅ mp4/aac | ✅ (webkit prefix) | Full support |
| Firefox | ✅ webm/opus | ❌ | Cloud transcription only |
| Edge | ✅ webm/opus | ✅ | Full support |
We use conventional commits for clear history:
feat: add entry CRUD API endpoints
fix: correct timer countdown calculation
test: add unit tests for streak calculation
docs: update README with testing guide
refactor: extract timer logic to composable
chore: upgrade Nuxt to 3.21Branch Strategy:
main— Always deployable, protectedfeature/description— Human-authored featurescopilot/description— AI agent-authored changes (auto-created by GitHub Copilot)
Pull Request Process:
- Create feature branch from
main - Make changes with tests
- Commit using conventional commit format
- Push and open PR (CI runs automatically)
- Get code review
- Merge to
main(squash merge preferred)
Goals:
- 80%+ unit test coverage target
- Critical user flows covered by E2E tests
- Co-locate tests with implementation
Test Structure:
app/
├── server/api/
│ ├── entries.get.ts
│ └── entries.get.test.ts ← API endpoint tests
├── composables/
│ ├── useTimer.ts
│ └── useTimer.test.ts ← Logic/composable tests
└── tests/e2e/
└── timer-flow.spec.ts ← Full user flow tests
Test-Driven Development:
- Write tests first when possible
- Test behavior, not implementation details
- Keep tests focused and readable
-
Create the endpoint file:
# For GET requests touch app/server/api/entries.get.ts # For POST requests touch app/server/api/entries.post.ts
-
Implement the handler:
// app/server/api/entries.get.ts import { db } from "~/server/db"; import { entries } from "~/server/db/schema"; export default defineEventHandler(async (event) => { const allEntries = await db.select().from(entries); return allEntries; });
-
Add tests:
// app/server/api/entries.get.test.ts import { describe, it, expect } from "vitest"; // ... test implementation
-
Test manually:
curl http://localhost:3000/api/entries
-
Create the Vue component:
touch app/pages/your-page.vue
-
Implement using Composition API:
<script setup lang="ts"> // Your component logic </script> <template> <div> <!-- Your template --> </div> </template>
-
Route auto-generates as
/your-page -
Update navigation in
layouts/default.vueif needed
-
Edit the schema file:
code app/server/db/schema.ts
-
Make your changes to the schema:
export const entries = sqliteTable("entries", { // Add new fields newField: text("new_field"), });
-
Generate migration:
cd app bun run db:generate -
Review the generated SQL in
drizzle/directory -
Apply the migration:
bun run db:migrate
-
Important: Commit both the schema file AND the migration files
Entry types are flexible (not an enum). Just use them:
const meditationEntry = {
type: "meditation",
data: {
technique: "vipassana",
location: "home",
},
};
const workoutEntry = {
type: "workout",
data: {
exercise: "running",
distance: 5.2,
unit: "km",
},
};No schema changes needed — type-specific data goes in the data JSONB field.
- TypeScript strict mode — Required, no implicit
any - ESLint + Prettier — Auto-format on save (configured)
- Vue 3 Composition API — Use
<script setup>syntax - Tailwind CSS — Prefer utility classes over custom CSS
- File naming:
- Pages:
kebab-case.vue - Components:
PascalCase.vue - Composables:
useFeatureName.ts - Tests:
*.test.tsor*.spec.ts
- Pages:
This project embraces GitHub Copilot and AI agents. You can leverage them to:
- Chat with
@workspace— Ask codebase questions - Use
/plan— Generate implementation plans - Use
/test— Generate test cases - Reference
#codebase— Workspace-wide searches
- Create an issue with detailed acceptance criteria
- Assign to
@copilot(requires GitHub Copilot Enterprise) - Agent autonomously creates a PR
- Review, iterate via PR comments, merge when ready
- See AGENTS.md for detailed agent instructions
- Design documents in
/designprovide context for agents
Every push to main or PR triggers automated checks:
- ✅ ESLint code style check
- ✅ TypeScript compilation
- ✅ Unit tests with coverage report
- ✅ Build verification
Merges to main additionally trigger:
- ✅ Docker image build
- ✅ Push to GitHub Container Registry (
ghcr.io)
| Problem | Solution |
|---|---|
| Module not found errors | Run cd app && bun install to refresh dependencies |
| Port 3000 already in use | Kill existing process: pkill -f 'bun.*dev'Or use different port: PORT=3001 bun run dev |
| Database errors | Delete app/data/db.sqlite* and restart (dev only!)Or check migrations ran: bun run db:migrate |
| TypeScript errors | Run bun run typecheck to see all errorsCheck you're using strict mode correctly |
| Tests not found | Ensure testing framework is installed Check package.json for test scripts |
| Hot reload not working | Restart dev server Check file is in watched directory |
| PWA not updating | Clear service worker cache Hard refresh: Cmd+Shift+R (Mac) / Ctrl+Shift+R (Windows) |
View logs:
# Server-side logs
cd app && bun run dev
# Browser console for client-side
# Open DevTools > ConsoleDatabase inspection:
cd app && bun run db:studio
# Opens Drizzle Studio on http://localhost:4983API testing:
# Test GET endpoint
curl http://localhost:3000/api/health
# Test POST endpoint with data
curl -X POST http://localhost:3000/api/entries \
-H "Content-Type: application/json" \
-d '{"type":"meditation","title":"Morning session"}'Tada follows these core principles (from design/philosophy.md):
- "Noticing, not tracking" — Focus on celebration, not obligation
- Data ownership — Users own their data, export must always work
- Offline-first — Must work without internet connection
- Simple > Complex — Avoid premature optimization
- Plugin architecture — Keep core minimal, extend via plugins
- ✅ Read this guide
- ✅ Read design/philosophy.md — Understand the "why"
- ✅ Read design/SDR.md — Detailed requirements
- ✅ Check design/roadmap.md — See what's planned
- ✅ Look at open issues on GitHub
- ✅ Start with a small contribution
See AGENTS.md for comprehensive agent-specific instructions, including:
- Project architecture details
- Testing strategies
- Commit message formats
- Common patterns and gotchas
We welcome contributions! Please:
- Start with an issue — Check existing issues or create a new one
- Follow conventions — Use conventional commits, co-locate tests
- Write tests — Aim for 80%+ coverage for new code
- Update docs — If you change behavior, update relevant docs
- Keep PRs focused — One feature/fix per PR
For major changes, discuss in an issue first to ensure alignment.
- Repository: https://github.com/InfantLab/tada
- License: AGPL-3.0
- Design Docs:
/design - Agent Instructions: AGENTS.md
Questions? Open an issue or check the design documents for guidance.