Toki2 is a provider-agnostic time tracking and development workflow platform:
- Track work time via pluggable time tracking providers (currently: Kleer)
- Monitor pull requests across pluggable SCM providers (currently: Azure DevOps)
- Real-time notifications for PR activity (comments, closures, mentions)
- Generate time entry notes from work items linked to PRs
Design goal: The system is designed to support multiple time tracking backends (not just Kleer) and multiple PR/issue providers (not just Azure DevOps). All new backend features should be built with this provider-agnosticism in mind.
Backend (Rust): Axum, SQLx (PostgreSQL), Tokio, azure_devops_rust_api, web-push
Frontend (React/TS): Vite, TanStack Router + Query, Zustand, shadcn/ui, Tailwind
Package manager: Bun — never use npm or npx, always use bun / bunx
toki-api/ # Main Axum backend
az-devops/ # Azure DevOps API wrapper crate
kleer/ # Kleer API client crate
app/ # React frontend
videos/ # fframes sources of product videos (standalone crates, outside the Cargo workspace)
Read .docs/kleer.md before Kleer-specific changes. It contains durable Kleer API URLs, service-account configuration, mapping rules, status/stat semantics, and implementation touchpoints.
jj (Jujutsu) is preferred over git. This repo uses jj for version control. Use jj commands instead of git when possible.
Hexagonal architecture (ports & adapters) is the target pattern for all backend domain logic. When adding or modifying backend features:
- Domain logic must not depend on specific providers. Business logic lives in
domain/services/and depends only on traits defined indomain/ports/. Never importaz_devops::*orkleer::*from domain code. - Define ports (traits) for external interactions. Inbound ports in
domain/ports/inbound/define use cases. Outbound ports indomain/ports/outbound/define what the domain needs from external systems. - Adapters implement ports. Provider-specific code lives in
adapters/outbound/{provider}/. HTTP handlers live inadapters/inbound/http/. - Prefer extending existing traits over creating provider-specific shortcuts. If a new capability is needed, add it to the relevant port trait and implement it in the adapter.
| Domain | Status | Notes |
|---|---|---|
| Time tracking | Fully hexagonal | TimeTrackingClient + TimeTrackingService ports, KleerAdapter |
| Work items | Fully hexagonal | WorkItemProvider + WorkItemService ports, AzureDevOpsWorkItemAdapter |
| Pull requests | Not abstracted | RepoDiffer directly uses az_devops::RepoClient — needs refactoring |
| Notifications | Not abstracted | Coupled to PR change events |
When working on partially or non-abstracted domains, prefer moving toward the hexagonal pattern rather than adding more provider coupling.
- Repository pattern: Database access via traits (
UserRepository, etc.) with*Implimplementations - AppState: Shared state container passed via Axum extractors
- RepoDiffer workers: Background tasks polling ADO for PR changes, communicating via mpsc channels (note: tightly coupled to Azure DevOps, future refactoring target)
- SQLx offline mode:
.sqlx/caches query metadata. SetSQLX_OFFLINE=trueto compile without a live DB. Runcargo sqlx prepareafter changing SQL queries
The time tracking system is the reference implementation of the hexagonal pattern. New domains should follow this structure:
toki-api/src/
├── domain/
│ ├── models/ # Domain types (ActiveTimer, TimeEntry, Project, etc.)
│ ├── ports/
│ │ ├── inbound/ # TimeTrackingService trait (use cases)
│ │ └── outbound/ # TimeTrackingClient, TimerHistoryRepository traits
│ ├── services/ # TimeTrackingServiceImpl (business logic)
│ └── error.rs # TimeTrackingError
└── adapters/
├── inbound/http/ # TimeTrackingServiceFactory, HTTP response types
└── outbound/
├── kleer/ # KleerAdapter (implements TimeTrackingClient)
└── postgres/ # PostgresTimerHistoryAdapter (implements TimerHistoryRepository)
Key traits:
TimeTrackingClient(outbound): Interface for time tracking providers (timer, projects, calendar)TimerHistoryRepository(outbound): Interface for local timer history storageTimeTrackingService(inbound): Use cases for HTTP handlersTimeTrackingServiceFactory: Creates service instances for authenticated Toki users
Per-request service creation: The service is created per request from the authenticated Toki user and the local Kleer mapping. Kleer credentials are server-side service-account config, not browser cookies or frontend state. The service merges provider data with local timer history for accurate start/end times.
- File-based routing: TanStack Router (
_layout/for layouts,$paramfor dynamic routes) - Query factories: Queries in factory objects returning
queryOptions() - Co-located components: Route-specific components in
-components/directories - Props inline: Inline props in function signatures, prefer
typeoverinterface
Use just to run common commands (run just to see all available recipes):
just dev # Run both backend and frontend
# Or individually:
just init-db # Initialize database (first time setup)
just run # Run backend
just app # Run frontend dev serverjust check-all # Verify everything (backend + frontend)
# Or individually:
just check # Backend - verify Rust compiles
just tsc # Frontend - TypeScript check
just lint # Frontend - ESLint
# Without a running database, use SQLX_OFFLINE=true:
SQLX_OFFLINE=true just checkDatabase tests (#[sqlx::test]) create and drop a database per test on the server in DATABASE_URL. Point it at a disposable local Postgres, never a shared or production one:
SQLX_OFFLINE=true DATABASE_URL=postgres://postgres:password@localhost:5433/postgres cargo test -p toki-apijust dev starts a standalone Aspire dashboard at http://127.0.0.1:18888 and exports OTEL over gRPC to http://127.0.0.1:18889.
When inspecting traces from the standalone dashboard, query it directly with --dashboard-url; aspire ps may report no AppHost because this repo runs the dashboard without an Aspire AppHost:
# A short trace id prefix works for trace lookup and returns the full trace id.
aspire otel traces \
--dashboard-url http://127.0.0.1:18888 \
--trace-id 3e7db67 \
--format Json \
--limit 20 \
--non-interactive \
--nologo
# Span lookup needs the full trace id from the trace result.
aspire otel spans \
--dashboard-url http://127.0.0.1:18888 \
--trace-id 3e7db67019bfc8a3b46bbdde72a791af \
--format Json \
--limit 200 \
--non-interactive \
--nologo- Provider-agnostic by default - New backend features should use ports/adapters, not couple directly to Kleer or Azure DevOps
- Kleer integration uses a service account - normal users must be locally mapped to Kleer users; read
.docs/kleer.mdfor Kleer-specific changes - Minimal test coverage - be careful with changes
- Coordinated changes - Backend API changes typically require frontend updates
- Route generation - Don't edit
routeTree.gen.tsmanually - shadcn/ui - Components in
app/src/components/ui/are from shadcn/ui - Timer buddy sprites are generated -
app/public/buddy/andapp/src/lib/buddy/species.generated.tscome fromapp/scripts/build_buddy_sprites.py(PMD SpriteCollab, CC BY-NC 4.0). Regenerate them with the script instead of editing by hand, and keepCREDITS.mdwith them
Backend config: toki-api/config/{base,local,production}.yaml + TOKI_* env vars
Required secrets: Azure AD OAuth credentials, Kleer integration token and company id