Development Workflow
Initial Setup
Prerequisites
- Node.js v22+ (recommended - check package.json engines field)
- npm (comes with Node.js)
- Git
Clone and Install
# Clone repository
git clone https://github.com/chrisvel/tududi.git
cd tududi
# Install all dependencies
# This installs both frontend and backend dependencies (monorepo setup)
npm install
Initialize Database
# Create database and run all migrations
npm run db:init
# This command:
# 1. Creates /backend/database.sqlite
# 2. Runs all migrations from /backend/migrations/
# 3. Sets up tables and relationships
Create Test User (Optional)
npm run user:create
# Interactive prompts:
# - Email address
# - Password
# - Timezone (defaults to system timezone)
Daily Development
Tududi runs two separate processes during development:
Two-Server Development
Terminal 1 - Backend (Express):
npm run backend:dev
# Details:
# - Runs: nodemon backend/app.js
# - Server: http://localhost:3002
# - Auto-reloads: Yes (on file changes)
# - API endpoints: /api/v1/*
# - Swagger docs: /api-docs (after login)
Terminal 2 - Frontend (Webpack Dev Server):
npm run frontend:dev
# Details:
# - Runs: webpack serve --mode development
# - Server: http://localhost:8080
# - Hot reload: Yes (React Fast Refresh)
# - Proxies /api/* to backend:3002
# - Proxies /locales/* to backend:3002
Or run both simultaneously:
npm start
# Runs both backend:dev and frontend:dev in parallel
# Uses 'concurrently' package
# Logs from both processes interleaved
Accessing the Application
Open http://localhost:8080 in your browser.
Login with:
- Email: The email you set during db:init or user:create
- Password: The password you set
Environment Variables
Create /backend/.env file (not tracked in git):
# Required
TUDUDI_SESSION_SECRET=your-random-secret-here-use-openssl-rand-hex-64
TUDUDI_USER_EMAIL=admin@example.com
TUDUDI_USER_PASSWORD=your-secure-password
# Optional - Server config
NODE_ENV=development
DB_FILE=database.sqlite
FRONTEND_URL=http://localhost:8080
BACKEND_URL=http://localhost:3002
PORT=3002
HOST=0.0.0.0
# Optional - Email
ENABLE_EMAIL=false
EMAIL_SMTP_HOST=smtp.example.com
EMAIL_SMTP_PORT=587
EMAIL_SMTP_SECURE=false
EMAIL_SMTP_USERNAME=user
EMAIL_SMTP_PASSWORD=pass
EMAIL_FROM_ADDRESS=noreply@example.com
EMAIL_FROM_NAME=Tududi
# Optional - Integrations
DISABLE_TELEGRAM=false
GOOGLE_CLIENT_ID=your-google-oauth-client-id
GOOGLE_CLIENT_SECRET=your-google-oauth-secret
GOOGLE_REDIRECT_URI=http://localhost:8080/auth/google/callback
# Optional - Features
DISABLE_SCHEDULER=false
SWAGGER_ENABLED=true
RATE_LIMITING_ENABLED=true
# Optional - Proxy (if behind reverse proxy)
TUDUDI_TRUST_PROXY=false
TUDUDI_ALLOWED_ORIGINS=http://localhost:8080
# Optional - Registration
REGISTRATION_TOKEN_EXPIRY_HOURS=24
Generate secure session secret:
openssl rand -hex 64
Adding a New Feature (Complete Example)
Example: Add "estimated_time" field to tasks
This walkthrough shows all files to touch when adding a new field to an existing model.
Step 1: Create Database Migration
npm run migration:create -- --name add-estimated-time-to-tasks
Edit the created file /backend/migrations/YYYYMMDDHHMMSS-add-estimated-time-to-tasks.js:
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.addColumn('Tasks', 'estimated_time', {
type: Sequelize.INTEGER, // minutes
allowNull: true,
defaultValue: null
});
},
async down(queryInterface, Sequelize) {
await queryInterface.removeColumn('Tasks', 'estimated_time');
}
};
Run migration:
npm run migration:run
Step 2: Update Sequelize Model
Edit /backend/models/task.js:
Task.init({
// ... existing fields ...
estimated_time: {
type: DataTypes.INTEGER,
allowNull: true,
comment: 'Estimated time in minutes'
},
// ... rest of fields ...
}, {
sequelize,
modelName: 'Task',
tableName: 'Tasks'
});
Step 3: Update Serializer (API Response)
Edit /backend/modules/tasks/core/serializers.js:
function serializeTask(task) {
return {
// ... existing fields ...
estimated_time: task.estimated_time,
// ... rest of fields ...
};
}
Step 4: Update Builder (API Input)
Edit /backend/modules/tasks/core/builders.js:
function buildTaskAttributes(data, userId) {
const attributes = {
// ... existing fields ...
estimated_time: data.estimated_time ? parseInt(data.estimated_time, 10) : null,
// ... rest of fields ...
};
return attributes;
}
Step 5: Add Validation (Optional)
If validation needed, edit /backend/modules/tasks/routes.js:
router.put('/task/:id', async (req, res, next) => {
try {
// Validate estimated_time
if (req.body.estimated_time !== undefined) {
const time = parseInt(req.body.estimated_time, 10);
if (isNaN(time) || time < 0) {
return res.status(400).json({
error: 'Estimated time must be a positive number'
});
}
}
// ... rest of route handler ...
} catch (error) {
next(error);
}
});
Step 6: Update Swagger Documentation
Edit /backend/config/swagger.js:
// Find Task schema
components: {
schemas: {
Task: {
type: 'object',
properties: {
// ... existing properties ...
estimated_time: {
type: 'integer',
description: 'Estimated time in minutes',
nullable: true,
example: 30
}
}
}
}
}
Step 7: Update Frontend TypeScript Interface
If TypeScript interface exists, edit /frontend/entities/Task.ts:
export interface Task {
// ... existing fields ...
estimated_time: number | null;
// ... rest of fields ...
}
Step 8: Update Frontend Component
Edit /frontend/components/Task/TaskForm.tsx:
// Add input field
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Estimated Time (minutes)
</label>
<input
type="number"
min="0"
value={task.estimated_time || ''}
onChange={(e) => updateTask({
...task,
estimated_time: e.target.value ? parseInt(e.target.value) : null
})}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
/>
</div>
Step 9: Write Tests
Add tests in /backend/tests/integration/tasks/tasks.test.js:
it('should create task with estimated_time', async () => {
const response = await request(app)
.post('/api/v1/task')
.set('Cookie', authCookie)
.send({
name: 'Task with estimate',
estimated_time: 60
});
expect(response.status).toBe(201);
expect(response.body.estimated_time).toBe(60);
});
it('should reject negative estimated_time', async () => {
const response = await request(app)
.post('/api/v1/task')
.set('Cookie', authCookie)
.send({
name: 'Invalid task',
estimated_time: -10
});
expect(response.status).toBe(400);
});
Step 10: Run Tests and Checks
# Run backend tests
npm run backend:test
# Run linting
npm run lint:fix
# Format code
npm run format:fix
# Run all pre-push checks
npm run pre-push
Step 11: Commit Changes
git add .
git commit -m "Add estimated_time field to tasks
- Add database migration for estimated_time
- Update Task model and serializers
- Update Swagger documentation
- Add validation for positive values
- Add UI field in TaskForm component
- Add integration tests"
Database Management
# Reset database (WIPES ALL DATA!)
npm run db:reset
# Seed development data
npm run db:seed
# Check migration status
npm run db:status
# Create new migration
npm run migration:create -- --name description
# Run pending migrations
npm run migration:run
# Rollback last migration
npm run migration:undo
Code Quality
# Check linting
npm run lint
# Auto-fix linting issues
npm run lint:fix
# Format code with Prettier
npm run format:fix
# Run all pre-push checks
# (lint + format + tests)
npm run pre-push
Testing
# Backend tests
npm test
# or
npm run backend:test
# Frontend tests
npm run frontend:test
# E2E tests
npm run test:ui # Headless
npm run test:ui:headed # With browser visible
# Coverage report
npm run test:coverage
# Watch mode (during development)
npm run test:watch
Branch Strategy
From CONTRIBUTING.md conventions:
# Feature branches
git checkout -b feature/description
# Bug fix branches
git checkout -b fix/description
# Refactoring branches
git checkout -b refactor/description
# Documentation branches
git checkout -b docs/description
# Test branches
git checkout -b test/description
Example Workflow
# Create feature branch from main
git checkout main
git pull origin main
git checkout -b feature/estimated-time
# Make changes, run tests
npm run pre-push
# Commit changes
git add .
git commit -m "Add estimated time feature"
# Before PR: rebase on main
git checkout main
git pull origin main
git checkout feature/estimated-time
git rebase main
# Push and create PR
git push origin feature/estimated-time
Build for Production
# Build frontend
npm run build
# Builds to: /dist/
# - Minified JavaScript
# - Optimized CSS
# - Hashed filenames for cache busting
Docker Production
# Build Docker image
docker build -t tududi:latest .
# Run container
docker run \
-e TUDUDI_USER_EMAIL=admin@example.com \
-e TUDUDI_USER_PASSWORD=secure-password \
-e TUDUDI_SESSION_SECRET=$(openssl rand -hex 64) \
-v ~/tududi_db:/app/backend/db \
-v ~/tududi_uploads:/app/backend/uploads \
-p 3002:3002 \
-d tududi:latest
Troubleshooting
Port Already in Use
# Find process using port 3002
lsof -ti:3002
# Kill process
kill -9 $(lsof -ti:3002)
# Or use different port
PORT=3003 npm run backend:dev
Database Locked
# Stop all servers
# Delete database file
rm backend/database.sqlite
# Reinitialize
npm run db:init
Module Not Found
# Clean install
rm -rf node_modules package-lock.json
npm install
Webpack Build Errors
# Clear webpack cache
rm -rf node_modules/.cache
# Rebuild
npm run frontend:dev