Modern, self-hosted issue tracker with Kotlin/Spring Boot backend and Next.js frontend. Features a powerful Kanban board, backlog management, advanced search, and drag & drop functionality.
- Kotlin 1.9+ - Modern JVM language
- Spring Boot 3.x - Application framework
- Spring WebFlux - Reactive web framework
- Spring Data R2DBC - Reactive database access
- Kotlin Coroutines - Suspend functions & Flow for async operations
- PostgreSQL - Database (required)
- Flyway - Database migrations
- Gradle (Kotlin DSL) - Build tool
- Java 21 - Runtime
- Next.js 16 - React framework (App Router)
- React 19 - UI library
- TypeScript 5.9 - Type safety (strict mode)
- Tailwind CSS - Utility-first styling
- @dnd-kit/react - Accessible drag & drop
- Static Export - Served from backend
/static
- Create unlimited boards for projects, teams, or workflows
- Quick board switching via sidebar
- Filter boards by name (client-side search)
- Board state persisted in URL (
?board={uuid})
Board View (Kanban)
- 4 status columns: To Do, In Progress, Ready for Deployment, Done
- Drag & drop tasks between columns
- Keyboard navigation support
Backlog View
- Custom categories (e.g., "Critical", "Tech Debt", "Nice to have")
- Drag & drop to reorder categories by priority
- Drag & drop tasks within and between categories
- Uncategorized section for unassigned tasks
- Quick promotion from Backlog to To Do
Archive View
- Review completed tasks
- Search archived tasks
- Restore tasks to active columns
- Structured Query Syntax:
Board:[Name]- Search specific boardTag:[TagName]- Filter by tagStatus:[StatusName]- Filter by status
- Query Chips - Structured filters become visual bubbles
- Autocomplete - Field names and values suggested
- Board/Global Scope - Search current board or all boards
- Keyboard Shortcut -
Cmd+K/Ctrl+Kto focus search - Debounced - 300ms delay to reduce API load
- Multiple tags per task
- Tag autocomplete based on existing tags
- Visual tag chips in UI
- Search integration
- Desktop: Full multi-column layout with persistent sidebar
- Mobile: Collapsible sidebar, touch-optimized drag & drop
- Touch support: Long-press to drag, swipe-friendly
- Optimistic UI updates (instant feedback)
- Reactive backend (non-blocking I/O)
- Indexed database queries
- Client-side filtering where possible
issue-tracker/
├── backend/
│ └── src/main/
│ ├── kotlin/com/issuetracker/
│ │ ├── domain/ # Entities (Board, Task, BacklogCategory)
│ │ ├── dto/ # Request/Response DTOs
│ │ ├── repository/ # R2DBC repositories
│ │ ├── service/ # Business logic
│ │ ├── web/ # REST controllers
│ │ └── exception/ # Custom exceptions
│ └── resources/
│ ├── db/migration/ # Flyway SQL migrations (V1__, V2__, ...)
│ ├── application.properties # Main config
│ └── static/ # Deployed frontend (auto-generated)
├── frontend/
│ ├── app/ # Next.js App Router pages
│ ├── components/ # React components
│ ├── lib/ # API client, utilities
│ ├── types/ # TypeScript interfaces
│ ├── out/ # Build output (→ backend/static)
│ └── next.config.mjs # Static export config
├── gradlew # Gradle wrapper
├── build.gradle.kts # Root build config
├── CLAUDE.md # Development guidelines
├── FEATURES.md # Detailed feature documentation
└── openapi.json # API specification
The easiest way to run Issue Tracker is using Docker Compose with PostgreSQL:
docker-compose.yml:
version: '3.8'
services:
postgres:
image: postgres:16-alpine
container_name: issue-tracker-db
environment:
POSTGRES_DB: issuetracker
POSTGRES_USER: issuetracker
POSTGRES_PASSWORD: changeme
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U issuetracker"]
interval: 10s
timeout: 5s
retries: 5
issue-tracker:
image: ghcr.io/freefair/issue-tracker:latest
container_name: issue-tracker
depends_on:
postgres:
condition: service_healthy
ports:
- "8080:8080"
environment:
# Spring Profile
SPRING_PROFILES_ACTIVE: prod
# Database Configuration
DB_HOST: postgres
DB_PORT: 5432
DB_NAME: issuetracker
DB_USER: issuetracker
DB_PASSWORD: changeme
# CORS Configuration (set to your domain in production)
CORS_ALLOWED_ORIGINS: http://localhost:8080
restart: unless-stopped
volumes:
postgres_data:Start:
docker-compose up -dView logs:
docker-compose logs -f issue-trackerStop:
docker-compose downAccess the application at http://localhost:8080
If you already have a PostgreSQL database:
docker run -d \
--name issue-tracker \
-p 8080:8080 \
-e SPRING_PROFILES_ACTIVE=prod \
-e DB_HOST=your-db-host \
-e DB_PORT=5432 \
-e DB_NAME=issuetracker \
-e DB_USER=your-db-user \
-e DB_PASSWORD=your-db-password \
-e CORS_ALLOWED_ORIGINS=https://yourdomain.com \
ghcr.io/freefair/issue-tracker:latestFor development or custom builds, see the Getting Started section below.
- Java 21 (via jenv, sdkman, or manual install)
- Node.js 18+ and npm (for frontend development)
- PostgreSQL 12+ (for local development)
-
Setup PostgreSQL database:
# Create database and user psql -U postgres -c "CREATE DATABASE issuetracker;" psql -U postgres -c "CREATE USER issuetracker WITH PASSWORD 'postgres';" psql -U postgres -c "GRANT ALL PRIVILEGES ON DATABASE issuetracker TO issuetracker;"
-
Build and deploy frontend:
cd frontend npm install npm run build # Builds to out/ npm run deploy # Copies out/ → backend/src/main/resources/static/
-
Start backend:
cd .. ./gradlew bootRun # Uses dev profile with localhost PostgreSQL
-
Access application:
- Frontend: http://localhost:8080
- API: http://localhost:8080/api
- Health Check: http://localhost:8080/health
Frontend hot reload:
cd frontend
npm run dev # Starts Next.js dev server on http://localhost:3000Backend auto-restart:
./gradlew bootRun --continuousFull deployment (after changes):
# Kill existing backend
pkill -f "gradle.*bootRun"
# Build and deploy frontend
cd frontend && npm run deploy
# Restart backend
cd ..
./gradlew bootRun > /tmp/backend.log 2>&1 &
# Verify
sleep 5 && curl -s http://localhost:8080/api/boards | jqGET /api/boards # List all boards
GET /api/boards/{id} # Get board by ID
POST /api/boards # Create board
PUT /api/boards/{id} # Update board
DELETE /api/boards/{id} # Delete board (cascades to tasks)GET /api/boards/{boardId}/tasks # List tasks for board
GET /api/boards/{boardId}/tasks?status=TODO # Filter by status
GET /api/tasks/{id} # Get task by ID
POST /api/boards/{boardId}/tasks # Create task
PATCH /api/tasks/{id} # Update task (partial)
PATCH /api/tasks/{id}/move # Move task (status + position)
DELETE /api/tasks/{id} # Delete taskGET /api/tasks/search?boardId={id}&q={query} # Search in board
GET /api/tasks/search/global?q={query} # Search across all boardsGET /api/boards/{boardId}/tags # Get all tags for board
GET /api/boards/{boardId}/tags?q={query} # Filter tagsGET /api/boards/{boardId}/backlog-categories # List categories
POST /api/boards/{boardId}/backlog-categories # Create category
GET /api/backlog-categories/{id} # Get category
PATCH /api/backlog-categories/{id} # Update category
DELETE /api/backlog-categories/{id} # Delete categoryCreate Board:
curl -X POST http://localhost:8080/api/boards \
-H "Content-Type: application/json" \
-d '{"name": "My Project", "description": "Main development board"}'Create Task:
curl -X POST http://localhost:8080/api/boards/{boardId}/tasks \
-H "Content-Type: application/json" \
-d '{
"title": "Implement user authentication",
"description": "Add JWT-based auth",
"status": "TODO",
"position": 0,
"tags": ["backend", "security"]
}'Update Task:
curl -X PATCH http://localhost:8080/api/tasks/{taskId} \
-H "Content-Type: application/json" \
-d '{"status": "IN_PROGRESS", "position": 1}'Search Tasks:
curl "http://localhost:8080/api/tasks/search?boardId={id}&q=authentication"
curl "http://localhost:8080/api/tasks/search/global?q=Tag:backend"Tables:
boards- Board definitionstasks- All tasks (linked to boards)backlog_categories- Custom backlog categories per boardflyway_schema_history- Migration tracking
Key Relationships:
tasks.board_id→boards.id(CASCADE DELETE)tasks.backlog_category_id→backlog_categories.id(SET NULL)backlog_categories.board_id→boards.id(CASCADE DELETE)
Database schema managed with Flyway. Migrations located in:
backend/src/main/resources/db/migration/
├── V1__initial_schema.sql
├── V2__add_backlog_categories.sql
└── ...
Creating new migrations:
- Create file:
V{N}__{description}.sql - Use sequential version numbers (V1, V2, V3, ...)
- Flyway auto-applies on next backend start
- Never modify existing migrations - always create new ones
Reset database:
# Drop and recreate database
psql -U postgres -c "DROP DATABASE IF EXISTS issuetracker;"
psql -U postgres -c "CREATE DATABASE issuetracker;"
psql -U postgres -c "GRANT ALL PRIVILEGES ON DATABASE issuetracker TO issuetracker;"
# Restart application - Flyway will run migrations
pkill -f gradle
./gradlew bootRunFrontend:
NEXT_PUBLIC_API_URL- API base URL (default:http://localhost:8080/api)
Backend:
Database Configuration:
DB_HOST- Database host (default:localhost)DB_PORT- Database port (default:5432)DB_NAME- Database name (default:issuetracker)DB_USER- Database user (default:issuetracker)DB_PASSWORD- Database password (default:changeme)
Application Configuration:
SPRING_PROFILES_ACTIVE- Spring profile (devorprod)dev- Development settings (hardcoded localhost PostgreSQL)prod- Production settings (uses environment variables)
CORS_ALLOWED_ORIGINS- Required for production - Comma-separated allowed origins (e.g.,https://yourdomain.com,https://www.yourdomain.com)SERVER_PORT- Server port (default:8080)LOGGING_LEVEL_ROOT- Log level (default:INFO)
Java Version:
# Using jenv
jenv local 21
# Or set JAVA_HOME
export JAVA_HOME=/path/to/java-21# 1. Build frontend (static export)
cd frontend
npm run build
npm run deploy
# 2. Build backend JAR (includes frontend)
cd ..
./gradlew build
# 3. JAR location
# backend/build/libs/backend-*.jarjava -jar backend/build/libs/backend-*.jarRequired:
SPRING_PROFILES_ACTIVE=prod- Enable production profile- Database connection (see below)
CORS_ALLOWED_ORIGINS- Your production domain(s)
PostgreSQL Database Setup:
First, create the database:
CREATE DATABASE issuetracker;
CREATE USER issuetracker WITH PASSWORD 'your_secure_password';
GRANT ALL PRIVILEGES ON DATABASE issuetracker TO issuetracker;Run with PostgreSQL:
export SPRING_PROFILES_ACTIVE=prod
export DB_HOST=localhost
export DB_PORT=5432
export DB_NAME=issuetracker
export DB_USER=issuetracker
export DB_PASSWORD=your_secure_password
export CORS_ALLOWED_ORIGINS=https://tracker.example.com
java -jar backend/build/libs/backend-*.jarOr use a .env file and docker-compose (see Installation)
R2DBC Persistable Pattern:
- Entities implement
Persistable<UUID>interface - Manual UUID generation via
UUID.randomUUID() - Call
.withPersistedFlag()after loading from DB
Reactive Streams:
- Kotlin
Flowfor streaming responses - Suspend functions for single-value responses
- Non-blocking I/O throughout
Optimistic UI Updates:
- Immediate local state update on user action
- Backend update in background
- Revert only on error
URL State Management:
- Board selection:
?board={uuid} - View selection:
?view=board|backlog|archive - Both required for full state restoration
Drag & Drop (@dnd-kit/react):
- Unified API across all views
- Position recalculation from 0 on every move
- Update ALL affected tasks, never use relative positions
View backend logs:
tail -f /tmp/backend.logCheck health:
curl http://localhost:8080/health
curl http://localhost:8080/api/boardsFrontend dev server:
cd frontend
npm run dev
# Access at http://localhost:3000Clean build:
rm -rf frontend/out frontend/.next backend/src/main/resources/static- CLAUDE.md - Development guidelines, architecture patterns, gotchas
- FEATURES.md - Detailed feature descriptions and workflows
- openapi.json - Complete API specification (OpenAPI 3.0)
- Clean Architecture - Clear separation: Domain, Repository, Service, Controller
- Type Safety - TypeScript (strict) + Kotlin type system
- Reactive - Non-blocking I/O with coroutines and Flow
- Validation - Jakarta Bean Validation on all inputs
- Error Handling - Consistent exception handling with meaningful messages
Current State (Development):
- No authentication (single-user mode)
- CORS disabled by default
- Input validation via Bean Validation
- XSS prevention via React auto-escaping
Production Requirements:
⚠️ SetCORS_ALLOWED_ORIGINSenvironment variable- Add authentication (JWT, OAuth, etc.)
- Enable HTTPS
- Add rate limiting
- Review and sanitize all inputs
MIT
For detailed development guidelines, see CLAUDE.md. For feature documentation, see FEATURES.md. For API details, see openapi.json.