# Rhythm Backend Blueprint (Rust/Axum) ## IDEAS - instead of elastic search use ts vector and ts query to allow easy issue searching for smooth experience - YouTrack competitor: issue tracker with organizations, projects, issues, and project-level RBAC --- ## Chosen Stack - **Language:** Rust (edition 2024) - **Framework:** Axum - **Runtime:** Tokio - **Migrations:** sqlx built-in migrations - **DB Access:** sqlx (compile-time checked queries, hand-written repositories) - **PostgreSQL Driver/Pool:** sqlx::PgPool - **Logging:** tracing + tracing-subscriber + tracing-tree (structured, hierarchical) - **Auth:** jsonwebtoken (HS256 access tokens, 15min) + opaque refresh tokens (7d, stored hashed in DB via SHA-256) + argon2 (password hashing) - **API Docs:** utoipa (code-first OpenAPI generation) - **Health Probes:** liveness/readiness endpoints - **Deployment:** Docker Compose on self-hosted hardware --- ## Decisions Made 1. **JWT signing:** HS256 (shared secret from env, can migrate to RS256 later) 2. **Token model:** Access = JWT (HS256, 15min, stateless verification). Refresh = opaque UUID v4 string, stored SHA-256 hashed in DB (7 days, revocable). Logout deletes refresh token from DB; access token still valid until expiry. 3. **Roles:** Project-level roles (`admin`, `developer`, `reporter`) + Org-level roles (`owner`, `admin`, `member`) 4. **OpenAPI workflow:** Code-first with utoipa (auto-generate spec from Rust handlers/models) 5. **DB access:** sqlx with hand-written repositories (compile-time checked, no ORM) 6. **First entities:** Users → Organizations → Projects → Issues 7. **Sprint stages:** Custom per project (project admin defines board columns like Todo → In Review → Done) 8. **Assignees:** Multiple assignees per issue (join table) 9. **Issue content features:** Tags, comments, issue relations, time tracking 10. **Plugin system:** Lua scripting with cron + event-driven triggers (Phase 7) --- ## Architecture Direction ### Project Structure ``` src/ ├── main.rs # Entrypoint, server, graceful shutdown ├── config.rs # Config (db_url, jwt_secret, socket_address, app_env) ├── database.rs # PgPool setup, migration runner ├── logging.rs # tracing config (dev: pretty console, prod: JSON + file) ├── errors.rs # AppError enum → IntoResponse (BadRequest/Unauthorized/Internal) ├── state.rs # AppState (PgPool, jwt_secret) ├── server.rs # Axum server init + graceful shutdown ├── auth/ │ ├── mod.rs │ ├── jwt.rs # HS256 JWT creation/validation (access tokens, 15min) │ ├── hash.rs # argon2 password hashing/verification │ └── token.rs # Refresh token generation (UUID v4) + SHA-256 hashing for DB storage ├── db/ │ ├── mod.rs │ ├── user_repo.rs # find_by_email, create │ └── token_repo.rs # store, find_by_hash, delete_by_hash, delete_all_for_user ├── service/ │ ├── mod.rs │ └── auth_service.rs # login, register, refresh, logout business logic ├── controller/ │ ├── mod.rs # Router composition (/api/v1/...) │ ├── model/ │ │ ├── mod.rs │ │ └── auth_model.rs # LoginRequest, RegisterRequest, AuthResponse DTOs │ └── v1/ │ ├── mod.rs # v1 router (nests /auth) │ └── auth_controller.rs # POST /login, /register, /refresh, /logout handlers ├── middleware/ │ ├── auth.rs # JWT extraction from Authorization header, inject CurrentUser │ └── rbac.rs # Project-level role guard ├── models/ # (future: org, project, issue, etc.) └── handlers/ # (future: orgs, projects, issues, etc.) migrations/ ├── 001_create_users.sql ├── 002_create_organizations.sql ├── 003_create_memberships.sql ├── 004_create_projects.sql ├── 005_create_stages.sql ├── 006_create_issues.sql ├── 007_create_issue_assignees.sql ├── 008_create_tags.sql ├── 009_create_comments.sql ├── 010_create_issue_relations.sql ├── 011_create_time_entries.sql ├── 012_create_sprints.sql ├── 013_create_refresh_tokens.sql └── 014_add_tsvector_search.sql ``` ### Layering Principles - Handlers stay thin — they extract, validate, call service, respond - Service layer owns business logic + DB transactions - Repositories own SQL queries (sqlx compile-time checked) - No transaction logic in handlers - AppState shared via Axum's State extractor ### Transaction Strategy - Service layer owns DB transactions - Repositories accept either `&PgPool` or `&mut Transaction` - sqlx compile-time query checking ensures SQL correctness at build time --- ## API and Runtime ### API Shape - REST JSON API - Possible future WebSocket support for interactive features - Versioning: `/api/v1` ### Health Endpoints - `GET /api/v1/health/live` - process is alive - `GET /api/v1/health/ready` - DB ping succeeds (and optionally migration version check) ### Auth Endpoints - `POST /api/v1/auth/register` - create account (email + password) - `POST /api/v1/auth/login` - get access + refresh tokens - `POST /api/v1/auth/refresh` - rotate refresh token ### Organization Endpoints - `POST /api/v1/orgs` - create org (creator becomes owner) - `GET /api/v1/orgs` - list user's orgs - `GET /api/v1/orgs/{org_slug}` - get org - `PATCH /api/v1/orgs/{org_slug}` - update org (owner/admin) - `DELETE /api/v1/orgs/{org_slug}` - delete org (owner) ### Project Endpoints - `POST /api/v1/orgs/{org_slug}/projects` - create project (org owner/admin) - `GET /api/v1/orgs/{org_slug}/projects` - list projects - `GET /api/v1/orgs/{org_slug}/projects/{project_slug}` - get project - `PATCH /api/v1/orgs/{org_slug}/projects/{project_slug}` - update project (project admin) - `DELETE /api/v1/orgs/{org_slug}/projects/{project_slug}` - delete project (project admin) ### Issue Endpoints - `POST /api/v1/orgs/{org_slug}/projects/{project_slug}/issues` - create issue (reporter+) - `GET /api/v1/orgs/{org_slug}/projects/{project_slug}/issues` - list issues (with `?q=` search, `?tag=`, `?assignee=`, `?status=`) - `GET /api/v1/orgs/{org_slug}/projects/{project_slug}/issues/{number}` - get issue - `PATCH /api/v1/orgs/{org_slug}/projects/{project_slug}/issues/{number}` - update issue (developer+) - `DELETE /api/v1/orgs/{org_slug}/projects/{project_slug}/issues/{number}` - delete issue (admin) ### Issue Sub-resource Endpoints **Tags:** - `GET /api/v1/orgs/{org}/projects/{proj}/tags` - list project tags - `POST /api/v1/orgs/{org}/projects/{proj}/tags` - create tag (developer+) - `DELETE /api/v1/orgs/{org}/projects/{proj}/tags/{tag_id}` - delete tag (admin) - `PUT /api/v1/orgs/{org}/projects/{proj}/issues/{num}/tags` - set issue tags (developer+) **Comments:** - `GET /api/v1/orgs/{org}/projects/{proj}/issues/{num}/comments` - list comments - `POST /api/v1/orgs/{org}/projects/{proj}/issues/{num}/comments` - add comment (reporter+) - `PATCH /api/v1/orgs/{org}/projects/{proj}/issues/{num}/comments/{id}` - edit comment (author only) - `DELETE /api/v1/orgs/{org}/projects/{proj}/issues/{num}/comments/{id}` - delete comment (author or admin) **Assignees:** - `GET /api/v1/orgs/{org}/projects/{proj}/issues/{num}/assignees` - list assignees - `PUT /api/v1/orgs/{org}/projects/{proj}/issues/{num}/assignees` - set assignees (developer+) **Relations:** - `GET /api/v1/orgs/{org}/projects/{proj}/issues/{num}/relations` - list relations - `POST /api/v1/orgs/{org}/projects/{proj}/issues/{num}/relations` - add relation (developer+) - `DELETE /api/v1/orgs/{org}/projects/{proj}/issues/{num}/relations/{id}` - remove relation (developer+) **Time Tracking:** - `GET /api/v1/orgs/{org}/projects/{proj}/issues/{num}/time-entries` - list time entries - `POST /api/v1/orgs/{org}/projects/{proj}/issues/{num}/time-entries` - log time (developer+) - `DELETE /api/v1/orgs/{org}/projects/{proj}/issues/{num}/time-entries/{id}` - delete entry (author or admin) ### Sprint Endpoints - `POST /api/v1/orgs/{org}/projects/{proj}/sprints` - create sprint (developer+) - `GET /api/v1/orgs/{org}/projects/{proj}/sprints` - list sprints - `GET /api/v1/orgs/{org}/projects/{proj}/sprints/{id}` - get sprint - `PATCH /api/v1/orgs/{org}/projects/{proj}/sprints/{id}` - update sprint (developer+) - `PATCH /api/v1/orgs/{org}/projects/{proj}/sprints/{id}/start` - start sprint (admin) - `PATCH /api/v1/orgs/{org}/projects/{proj}/sprints/{id}/complete` - complete sprint (admin) - `GET /api/v1/orgs/{org}/projects/{proj}/sprints/{id}/board` - sprint board (issues grouped by stage) ### Stage Endpoints (project board columns) - `POST /api/v1/orgs/{org}/projects/{proj}/stages` - create stage (admin) - `PATCH /api/v1/orgs/{org}/projects/{proj}/stages` - reorder stages (admin) - `DELETE /api/v1/orgs/{org}/projects/{proj}/stages/{id}` - delete stage (admin) ### Logging - tracing with hierarchical layer (dev: pretty console, prod: JSON + file with rotation) - Correlation/request ID in middleware - Structured error logging from middleware and service boundaries --- ## Database Schema ```sql -- Enums CREATE TYPE org_role AS ENUM ('owner', 'admin', 'member'); CREATE TYPE project_role AS ENUM ('admin', 'developer', 'reporter'); CREATE TYPE issue_status AS ENUM ('open', 'in_progress', 'resolved', 'closed'); CREATE TYPE issue_priority AS ENUM ('critical', 'major', 'normal', 'minor'); CREATE TYPE issue_type AS ENUM ('bug', 'feature', 'task', 'improvement'); CREATE TYPE issue_relation_type AS ENUM ('blocks', 'is_blocked_by', 'duplicates', 'is_duplicated_by', 'relates_to', 'clones', 'is_cloned_by'); CREATE TYPE sprint_status AS ENUM ('planned', 'active', 'completed'); CREATE TYPE trigger_type AS ENUM ('event', 'schedule', 'manual'); -- Users CREATE TABLE users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, display_name TEXT, avatar_url TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- Organizations CREATE TABLE organizations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, slug TEXT NOT NULL UNIQUE, description TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- Org memberships (owner/admin/member) CREATE TABLE org_memberships ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, role org_role NOT NULL DEFAULT 'member', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE(user_id, org_id) ); -- Projects CREATE TABLE projects ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, slug TEXT NOT NULL, description TEXT, org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, created_by UUID NOT NULL REFERENCES users(id), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE(org_id, slug) ); -- Project memberships (admin/developer/reporter) CREATE TABLE project_memberships ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE, role project_role NOT NULL DEFAULT 'reporter', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE(user_id, project_id) ); -- Stages (custom board columns per project) CREATE TABLE stages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, position INT NOT NULL, project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE, UNIQUE(project_id, position) ); -- Issues CREATE TABLE issues ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), number BIGINT NOT NULL, title TEXT NOT NULL, description TEXT, status issue_status NOT NULL DEFAULT 'open', priority issue_priority NOT NULL DEFAULT 'normal', issue_type issue_type NOT NULL DEFAULT 'task', project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE, reporter_id UUID NOT NULL REFERENCES users(id), stage_id UUID REFERENCES stages(id) ON DELETE SET NULL, sprint_id UUID REFERENCES sprints(id) ON DELETE SET NULL, search_vector TSVECTOR GENERATED ALWAYS AS ( setweight(to_tsvector('english', coalesce(title, '')), 'A') || setweight(to_tsvector('english', coalesce(description, '')), 'B') ) STORED, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE(project_id, number) ); -- Multiple assignees per issue CREATE TABLE issue_assignees ( issue_id UUID NOT NULL REFERENCES issues(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, PRIMARY KEY (issue_id, user_id) ); -- Tags (per project) CREATE TABLE tags ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, color TEXT, project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE, UNIQUE(project_id, name) ); CREATE TABLE issue_tags ( issue_id UUID NOT NULL REFERENCES issues(id) ON DELETE CASCADE, tag_id UUID NOT NULL REFERENCES tags(id) ON DELETE CASCADE, PRIMARY KEY (issue_id, tag_id) ); -- Comments CREATE TABLE comments ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), issue_id UUID NOT NULL REFERENCES issues(id) ON DELETE CASCADE, author_id UUID NOT NULL REFERENCES users(id), body TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- Related issues CREATE TABLE issue_relations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), from_issue_id UUID NOT NULL REFERENCES issues(id) ON DELETE CASCADE, to_issue_id UUID NOT NULL REFERENCES issues(id) ON DELETE CASCADE, relation_type issue_relation_type NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE(from_issue_id, to_issue_id, relation_type) ); -- Time tracking CREATE TABLE time_entries ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), issue_id UUID NOT NULL REFERENCES issues(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users(id), seconds_spent INTEGER NOT NULL CHECK (seconds_spent > 0), description TEXT, logged_at TIMESTAMPTZ NOT NULL DEFAULT now(), created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- Sprints CREATE TABLE sprints ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE, starts_at TIMESTAMPTZ NOT NULL, ends_at TIMESTAMPTZ NOT NULL, status sprint_status NOT NULL DEFAULT 'planned', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- Refresh tokens CREATE TABLE refresh_tokens ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, token_hash TEXT NOT NULL UNIQUE, expires_at TIMESTAMPTZ NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- Scripts (for Lua plugin system) CREATE TABLE scripts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, source TEXT NOT NULL, trigger_type trigger_type NOT NULL, trigger_config JSONB, project_id UUID REFERENCES projects(id) ON DELETE CASCADE, org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, enabled BOOLEAN NOT NULL DEFAULT true, created_by UUID NOT NULL REFERENCES users(id), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE script_revisions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), script_id UUID NOT NULL REFERENCES scripts(id) ON DELETE CASCADE, source TEXT NOT NULL, created_by UUID NOT NULL REFERENCES users(id), created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE script_execution_logs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), script_id UUID NOT NULL REFERENCES scripts(id) ON DELETE CASCADE, status TEXT NOT NULL, output TEXT, error TEXT, duration_ms INTEGER, triggered_by TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- Indexes CREATE INDEX idx_issues_search ON issues USING GIN(search_vector); CREATE INDEX idx_issues_project_sprint ON issues(project_id, sprint_id); CREATE INDEX idx_issues_project_stage ON issues(project_id, stage_id); CREATE INDEX idx_time_entries_issue ON time_entries(issue_id); CREATE INDEX idx_comments_issue ON comments(issue_id); ``` **Note:** The `issues` table references `sprints(id)` via `sprint_id`. In practice, the `sprints` table migration must run before `issues` (or `sprint_id` is added in a later migration). The migration files are ordered to handle this. --- ## RBAC Model ### Org-Level Roles | Role | Capabilities | |---------|-------------| | owner | Delete org, transfer ownership, manage members, create/delete projects | | admin | Manage members, create/delete projects, update org settings | | member | View org and its projects, join projects | ### Project-Level Roles | Role | Capabilities | |-----------|-------------| | admin | Update/delete project, manage project members, delete issues, manage stages, start/complete sprints | | developer | Create issues, assign issues, update issue status/priority/stage, log time, add tags, add relations | | reporter | Create issues, comment, view issues | ### RBAC Implementation - `RequireRole` and `RequireRole` Axum extractors - Extract `CurrentUser` from JWT middleware - Query membership for the org/project (from path params) - Reject if not a member or insufficient role (401/403) - Org members automatically get `reporter` role in org's projects unless explicitly assigned --- ## Implementation Phases ### Phase 1: Foundation (Axum + DB + Health) **Dependencies:** `axum`, `tokio`, `sqlx` (postgres, runtime-tokio, tls-rustls, migrate, uuid, chrono), `serde`/`serde_json`, `uuid`, `chrono` **Tasks:** 1. Extend `Config` with `jwt_secret`, `db_url` parsing, `server_port` 2. Create `AppState` struct holding `PgPool` and `Config` 3. Set up `sqlx::PgPool` with `sqlx::migrate!().run()` on startup 4. Create health endpoints: `GET /api/v1/health/live` and `GET /api/v1/health/ready` 5. Wire up Axum router with `routes()`, shared state, and graceful shutdown via `tokio::signal` 6. Create first migration: `users` table ### Phase 2: Auth (Hybrid JWT Access + DB-stored Refresh Tokens) **Token model:** Access tokens are signed JWTs (15min, no DB lookup). Refresh tokens are opaque strings stored hashed in DB (7 days, revocable). **Additional dependencies:** `jsonwebtoken`, `argon2`, `sha2`, `uuid` (v4), `chrono` **Current progress:** - ✅ Config, AppState (PgPool), DB migrations runner, router setup, users migration - ✅ Error types (`AppError` with `IntoResponse` — BadRequest/Unauthorized/Internal) - ✅ Auth controller routes wired (`POST /api/v1/auth/login`, `/register`) - ✅ Auth DTOs (`LoginRequest`, `RegisterRequest`, `AuthResponse`) in `controller/model/auth_model.rs` - ✅ Service stubs (`auth_service.rs` — `login()`, `register()` with `todo!()`) - ⚠️ Handler parameter order needs fix (State before Json) **Tasks:** 1. ~~Create `users` migration~~ (done — `0001_create_users_table.sql`) 2. Fix handler parameter order — `State` before `Json` in `auth_controller.rs` 3. Create `tokens` migration — `0002_create_tokens_table.sql` (id, user_id, token_hash, expires_at, created_at) 4. Add dependencies to `Cargo.toml` — `argon2`, `jsonwebtoken`, `sha2`, `uuid` (v4), `chrono` 5. Create `src/db/` module — `user_repo.rs` (find_by_email, create) + `token_repo.rs` (store, find_by_hash, delete_by_hash, delete_all_for_user) 6. Create `src/auth/hash.rs` — argon2 password hashing (`hash_password`, `verify_password`) 7. Create `src/auth/jwt.rs` — HS256 JWT access token creation/validation with 15min expiry (`create_access_token`, `verify_access_token`, `Claims` struct) 8. Create `src/auth/token.rs` — generate random refresh token (UUID v4), SHA-256 hash for DB storage 9. Add `jwt_secret` to `AppState` / `Config` (loaded from `JWT_SECRET` env var) 10. Implement `auth_service.rs` — login (find user → verify password → create JWT access token + generate/store refresh token) and register (check email → hash password → create user → create tokens) 11. Add input validation — email/password not empty → `AppError::Validation` 12. Create `middleware::auth` — JWT extraction from `Authorization: Bearer `, inject `CurrentUser` 13. Add `POST /api/v1/auth/refresh` — accepts refresh token → hash → look up in DB → if valid, delete old + create new token pair 14. Add `POST /api/v1/auth/logout` — accepts refresh token → hash → delete from DB 15. Protect routes with auth layer ### Phase 3: RBAC Layer **Tasks:** 1. Create `organization_memberships` and `project_memberships` migrations 2. Define `OrgRole` and `ProjectRole` as SQL enums + Rust enums 3. Implement `db::repos::memberships` — add/remove/check, get role 4. Create `RequireRole` and `RequireRole` Axum extractors 5. Wire role guards into project/issue routes ### Phase 4: Core Domain (Organizations → Projects → Issues) **Tasks:** 1. **Organizations** — CRUD at `POST/GET/PATCH/DELETE /api/v1/orgs` - Only org owner/admin can update/delete - Org creator becomes owner automatically - Slug auto-generated from name 2. **Projects** — CRUD at `/api/v1/orgs/{org_slug}/projects` - Project membership inherits from org membership - Org owner/admin can create projects - Project admin can update project settings 3. **Issues** — CRUD at `/api/v1/orgs/{org_slug}/projects/{project_slug}/issues` - reporter+ can create issues - developer+ can assign/update status - admin can delete issues - `tsvector` column for full-text search on title + description - `GET .../issues?q=search+terms` uses `tsquery` 4. **Pagination** — cursor-based pagination on all list endpoints 5. **utoipa** — add `#[derive(OpenApi)]` annotations, expose `GET /api/v1/docs/openapi.json` ### Phase 5: Issue Rich Content (Tags, Comments, Relations, Time Tracking, Assignees) **Tasks:** 1. **Tags** — `tags` + `issue_tags` tables - Create/list/delete tags per project (developer+ to create, admin to delete) - Set issue tags (developer+) - Filter issues by tag: `GET .../issues?tag=bug` 2. **Comments** — `comments` table - List/create comments on issues (reporter+) - Edit own comment, delete own comment or admin 3. **Issue Relations** — `issue_relations` table with relation types - Add/remove relations (developer+) - Types: blocks, is_blocked_by, duplicates, is_duplicated_by, relates_to, clones, is_cloned_by 4. **Time Tracking** — `time_entries` table - Log time on issues (developer+) - Delete own entry or admin - Aggregate total time spent on issue (computed from entries) 5. **Multiple Assignees** — `issue_assignees` join table - Replace single `assignee_id` with join table - Set/replace assignees on issue (developer+) - Filter issues by assignee: `GET .../issues?assignee={user_id}` ### Phase 6: Sprints & Stages **Tasks:** 1. **Stages** — `stages` table (custom board columns per project) - Create/reorder/delete stages (project admin) - Default stages created with new project (e.g. Todo, In Progress, Done) - Issue `stage_id` tracks which column it's in 2. **Sprints** — `sprints` table - Sprint CRUD endpoints - Sprint state transitions: `planned` → `active` → `completed` - Issue-sprint assignment via `sprint_id` on issues (developer+) 3. **Sprint Board** — `GET .../sprints/{id}/board` - Returns issues grouped by stage within the sprint - Moving an issue between stages updates `stage_id` ### Phase 7: Polish & Hardening **Tasks:** 1. CORS middleware (configurable origins from env) 2. Request ID middleware (propagate through logging) 3. Rate limiting (e.g., `tower-governor`) 4. Comprehensive error responses (validation → 422, auth → 401/403) 5. Integration tests with test DB containers (`testcontainers`) 6. Update Dockerfile and compose.yaml for production-ready build ### Phase 8: Plugin System (Lua Scripting) **Additional dependencies:** `mlua` (lua54, vendored, async, serialize), `cron` (cron expression parsing) **Concept:** Users write Lua scripts triggered by events or schedules. Scripts get a sandboxed API to automate workflows (e.g., auto-migrate tasks when a sprint ends, auto-assign issues, send notifications). **Example user scripts:** ```lua -- Auto-resolve in-progress tasks when sprint ends on_event("sprint.ended", function(ctx) local issues = ctx.project:issues({ status = "in_progress", sprint = ctx.sprint.id }) for _, issue in ipairs(issues) do issue:update({ status = "resolved", comment = "Auto-resolved: sprint ended" }) end end) ``` ```lua -- Every Friday at 6pm, notify about stale issues on_schedule("0 18 * * 5", function(ctx) local stale = ctx.project:issues({ status = "open", updated_before = "7d" }) for _, issue in ipairs(stale) do issue:add_comment("This issue has been inactive for 7 days.") end end) ``` **Trigger models (cron + event-driven only):** | Trigger | Example | Implementation | |---------|---------|----------------| | Event-driven | `on_event("issue.created", ...)` | Hook into service layer, emit events after mutations | | Scheduled (cron) | `on_schedule("0 18 * * 5", ...)` | Cron expressions, background tokio task | **Available events:** - `issue.created`, `issue.updated`, `issue.deleted` - `issue.status_changed`, `issue.assigned`, `issue.unassigned` - `comment.created`, `comment.deleted` - `sprint.started`, `sprint.ended`, `sprint.completed` - `tag.created`, `tag.removed` **Sandboxed Lua API surface:** - `ctx.project:issues(filter)` — query issues - `ctx.project:sprints()` — list sprints - `issue:update(fields)` — mutate issue - `issue:add_comment(body)` — add comment - `ctx.user` — current user info **Safety model:** - No filesystem, no network access - Execution timeout (e.g., 5s max per script) - Memory limit via `mlua` Lua state options - Scripts run in separate tokio tasks, never block the API **Script storage:** - `scripts` table in Postgres (org_id, project_id, name, source, trigger_type, trigger_config, enabled) - `script_revisions` table for revision history / audit - `script_execution_logs` table for past runs, errors, output **Codebase location:** ``` src/ ├── scripting/ │ ├── mod.rs # Script engine, mlua setup, sandbox config │ ├── api.rs # Lua API bindings (ctx.project, ctx.issue, etc.) │ ├── scheduler.rs # Cron-based trigger runner │ ├── hooks.rs # Event emission from service layer │ └── models.rs # Script DB model + DTOs ``` **Tasks:** 1. Add `scripts`, `script_revisions`, and `script_execution_logs` tables to DB 2. Implement `scripting::mod` — mlua sandbox setup, script compilation/validation 3. Implement `scripting::api` — expose safe Lua API (ctx.project, ctx.issue, ctx.sprint) 4. Implement `scripting::hooks` — event emission from service layer 5. Implement `scripting::scheduler` — cron expression parsing + background tokio task runner 6. Script CRUD endpoints at `/api/v1/orgs/{org_slug}/projects/{project_slug}/scripts` 7. Script execution endpoint: `POST .../scripts/{id}/run` (for manual trigger / testing) 8. Script execution log endpoint: `GET .../scripts/{id}/logs` 9. Admin controls: enable/disable scripts per org/project, execution timeout config --- ## Implementation Order (within each feature) 1. **Migration** → write and run the SQL migration 2. **Repository** → write the sqlx queries + Rust repo struct 3. **Model** → define the Rust domain model + DTOs 4. **Service** → business logic (validation, authorization rules) 5. **Handler** → thin Axum handler calling service 6. **Route** → wire into the router with appropriate middleware 7. **Test** → write a test for the endpoint --- ## Testing Approach ### Phase 1 (Recommended Start) - Unit tests for pure service logic (no DB, mock repositories) - Integration tests for repositories with real Postgres via Docker ### Repository Testing - Define trait interfaces for repositories - For service unit tests, provide mock implementations - Focus unit tests on business rules, branching, and error mapping - Keep SQL correctness in integration tests against real Postgres - This split gives fast unit tests plus high-confidence DB integration coverage ### Phase 2 - HTTP handler tests with Axum test helpers - Auth middleware tests - RBAC extractor tests ### Phase 3 - Minimal end-to-end happy path tests - Load/stress testing --- ## OpenAPI + Frontend Type Generation - utoipa annotations on handlers and models - Auto-generate OpenAPI spec at build time - Expose at `GET /api/v1/docs/openapi.json` - Generate frontend TypeScript types from OpenAPI (e.g., `openapi-typescript`) - Optionally serve Swagger UI from backend via `utoipa-swagger-ui` --- ## Deployment - Docker Compose for app + postgres - Healthcheck in compose targets readiness endpoint - Env-based configuration (`.env`, `.env.example`) - Multi-stage Dockerfile (rust:alpine builder → alpine runtime) --- ## Iteration Notes - This file is intentionally a working draft. - We will refine decisions and turn this into a concrete implementation checklist. - Originally started as Go/Gin, pivoted to Rust/Axum for the Rust learning journey.