Aha.io is a powerful product management platform, but its UI is slow and bloated for the workflows a scrum master runs through every day. Loading a single backlog view can take seconds. Estimating features means clicking through multiple screens. There's no quick way to see who's over-committed in a sprint, track standup blockers over time, or compare velocity across sprints without exporting to a spreadsheet.
Scrum masters need a small, fast set of tools — not the full product management suite. Aha SMT pulls data from Aha via its REST API and presents focused views for the things that matter during a sprint: what needs estimating, who has capacity, what's blocking people, and how the team is trending.
- Backlog Grooming — View and filter unestimated features by release, assignee, and tags
- Three-Criteria Estimation — Score features across Scope, Complexity, and Unknowns with a suggested point value. Writes scores back to Aha. Keyboard shortcuts for fast flow.
- Sprint Planning — Per-member capacity vs allocation with days-off tracking. Visual capacity bars showing over/under-commitment. Supports both Aha iterations and releases.
- Standup Tracking — Daily standup entries with blocker aging (amber >2d, red >5d) and action item tracking
- Sprint Metrics — Capture sprint snapshots, track velocity over time, compare sprints side-by-side, and view per-member performance
- Org Configuration — Externalized config for point source priority, estimation scale, sprint mode, workflow statuses, and the estimation matrix. Customize per org without forking.
- Next.js 15 (App Router) + React 19 + TypeScript
- Tailwind CSS 4 — dark theme, no CSS-in-JS runtime
- TanStack React Query v5 — client-side caching
- Drizzle ORM + better-sqlite3 — local DB for standups, snapshots, days off
- recharts — velocity and metrics charts
- Vitest — 249 tests across 19 test files
- Node.js 18+
- An Aha API token (Settings > Personal > Developer)
# Install dependencies
npm install
# Configure environment
cp .env.example .env.local
# Edit .env.local with your Aha domain and API token
# Create org config (edit to customize)
cp aha-smt.config.example.ts aha-smt.config.ts
# Start dev server
npm run devOpen http://localhost:3000/settings to verify your Aha connection.
The app supports three layers of configuration, each overriding the previous:
- Settings UI — Primary method for most teams. Configure org settings at
/settings(stored in SQLite). - Environment Variables — For Docker/Kubernetes deployments or to pre-populate settings. All optional.
- File-based Config — Advanced use case for complex estimation matrices or multi-environment setups. Edit
aha-smt.config.tsdirectly.
The precedence order is: File Config > Environment Variables > Database (Settings UI) > Defaults
The easiest way to configure your org. No deployment needed — changes take effect immediately.
- Backlog Filter — Choose how to group features: by Release, Team Location, Epic, Tag, or Custom Field
- Points Source — Which Aha fields to read: Original Estimate, Score, or Work Units (in priority order)
- Points Scale — Valid point values for the estimation picker
- Points Per Day — Default capacity assumption (overridable per team member in Sprint Planning)
- Sprint Mode — Show Iterations, Releases, or both
- Workflow Complete Statuses — Which Aha workflow meanings count as "done" for metrics
- Estimation Matrix — Scope/Complexity/Unknowns lookup table (optional override)
Set these in .env.local for Docker/Kubernetes deployments. See .env.example for all options.
| Variable | Description |
|---|---|
AHA_DOMAIN |
Your Aha subdomain (e.g. mycompany for mycompany.aha.io) |
AHA_API_TOKEN |
API key from Aha Settings > Personal > Developer |
| Variable | Description |
|---|---|
AHA_DEFAULT_PRODUCT_ID |
Auto-select a product on load |
AHA_TEAM_PRODUCT_ID |
Aha Develop workspace ID for iteration support |
| Variable | Description | Default |
|---|---|---|
DATABASE_URL |
SQLite database path | file:./data/aha-smt.db |
CACHE_TTL_SECONDS |
Server-side cache TTL (seconds) | 60 |
These settings pre-populate the Settings UI. The UI values override these if both are set.
| Variable | Options | Description |
|---|---|---|
BACKLOG_FILTER_TYPE |
release | team_location | epic | tag | custom_field |
Backlog grouping strategy (default: release) |
BACKLOG_TEAM_PRODUCT_ID |
Product ID (number) | Required if BACKLOG_FILTER_TYPE=team_location — your Aha Develop workspace ID |
BACKLOG_EXCLUDE_WORKFLOW_KINDS |
Comma-separated kinds | Features to exclude from backlog (e.g. Bug,Test) |
POINTS_SOURCE |
Comma-separated fields | Priority order for point extraction: original_estimate, score, work_units. First non-null wins. (default: original_estimate,score) |
POINTS_SCALE |
Comma-separated numbers | Valid point values (default: 1,2,3,5,8,13,21) |
POINTS_DEFAULT_PER_DAY |
Number | Default capacity per team member (default: 1) |
SPRINTS_MODE |
iterations | releases | both |
Sprint display mode (default: both) |
SPRINTS_DEFAULT_VIEW |
iterations | releases |
Default tab when mode is both (default: iterations) |
WORKFLOW_COMPLETE_MEANINGS |
Comma-separated values | Aha workflow meanings that count as "done" (default: DONE,SHIPPED) |
ESTIMATION_MATRIX |
JSON string | Estimation lookup table override (advanced; see aha-smt.config.example.ts for format) |
For advanced use cases (complex estimation matrices, multi-environment setups), edit aha-smt.config.ts at the project root. The file is gitignored — each deployment maintains its own copy.
Copy aha-smt.config.example.ts to get started. The app works out of the box with defaults; only include the settings you want to override.
Precedence: File config values override environment variables and database settings.
| Setting | Type | Default | Description |
|---|---|---|---|
points.source |
string[] |
["original_estimate", "score"] |
Priority order for extracting points from a feature. First non-null value wins. |
points.scale |
number[] |
[1, 2, 3, 5, 8, 13, 21] |
Point values shown in the estimation UI |
points.defaultPerDay |
number |
1 |
Starting default for points-per-day capacity (overridable in Settings) |
sprints.mode |
"iterations" | "releases" | "both" |
"both" |
Which sprint types to show |
sprints.defaultView |
"iterations" | "releases" |
"iterations" |
Default tab when mode is "both" |
workflow.completeMeanings |
string[] |
["DONE", "SHIPPED"] |
Aha internalMeaning values that count as complete |
estimation.matrix |
Record<string, number> |
See example file | Scope/Complexity/Unknowns → Points lookup (keys like "L-M-H") |
Example customization:
import { defineConfig } from "@/lib/config";
export default defineConfig({
points: {
source: ["original_estimate"],
scale: [0.5, 1, 2, 3, 5, 8, 13],
},
sprints: {
mode: "iterations",
},
workflow: {
completeMeanings: ["DONE"],
},
});Only include the settings you want to override — everything else falls back to defaults via deep merge.
npm run dev # Start dev server (http://localhost:3000)
npm run build # Production build (verifies TypeScript + ESLint)
npm test # Run all tests (vitest)
npm run test:watch # Watch mode
npm run bench # Run micro-benchmarks (vitest bench)
npx tsx benchmarks/live-api-benchmark.ts # Live Aha API latency testAll Aha API calls go through Next.js Route Handlers (/api/aha/*) to keep the token server-side. Every request uses ?fields=... to minimize payload size. Responses are cached in-memory with configurable TTL. A token-bucket rate limiter (20 req/sec burst, 300 req/min sustained) prevents hitting Aha's rate limits.
The Aha REST API v1 handles most data (releases, features, users, products). However, the REST API has no support for iteration-feature linkage — the ?iteration= filter is silently ignored, and features don't have an iteration field. For sprint/iteration data, we use the Aha GraphQL v2 API (/api/v2/graphql), which returns iterations with their linked features in a single request.
| API | Used for | Why |
|---|---|---|
| REST v1 | Releases, features, users, products, auth | Mature, well-documented, fine for non-iteration data |
| GraphQL v2 | Iterations + their features | Only way to get iteration→feature linkage; 87% faster for sprint planning |
SQLite stores data that doesn't belong in Aha:
- Standup entries — daily updates per team member
- Blockers & action items — tracked across standups with aging and resolution
- Sprint snapshots — captured at sprint end for historical metrics
- Estimation history — criteria breakdown for calibration
- Days off — PTO and holidays affecting capacity calculations
- Settings — per-instance preferences (e.g. points-per-day overrides)
Tables are auto-created on first run — no migration step needed.
Org-specific behavior is externalized into aha-smt.config.ts at the project root. The defineConfig() helper provides type checking and IDE autocomplete. At runtime, getConfig() deep-merges the user config over DEFAULT_CONFIG and caches the result as a singleton.
The config drives:
- Point extraction —
points.tsiterates the configuredsourcearray instead of hardcodedoriginal_estimate ?? score - Estimation UI — point picker and suggested-points matrix read from config
- Sprint mode — sprint list page shows iterations, releases, or both with a toggle
- Workflow completion — GraphQL feature mapping uses configured
completeMeanings
The Aha! APIs are the primary bottleneck for responsiveness. We measured production latency against a real Aha! environment and applied every mitigation available, but some constraints are outside our control.
| Endpoint | Avg Latency | p95 | Notes |
|---|---|---|---|
/me (auth check) |
~430ms | ~600ms | Cold-start spikes to 700ms+ |
/products |
~185ms | ~195ms | Small payload |
/releases (per product) |
~260ms | ~390ms | Varies with release count |
/features (200 items, lean fields) |
~1.2s | ~1.5s | Full fields (incl. description): ~1.8s |
/features (full pagination, 3400 items) |
~29s | ~36s | 17 sequential pages at 200/page |
/features/:id (single detail) |
~215ms | ~225ms | With description + requirements |
/users (per product) |
~270ms | ~420ms | 30 users |
/iterations (list only, no features) |
~440ms | ~485ms | 21 iterations; cannot link features |
| Query | Avg Latency | p95 | Notes |
|---|---|---|---|
| Schema introspection | ~210ms | ~225ms | Baseline GraphQL overhead |
| All iterations + features (21 sprints, 1626 features) | ~2.7s | ~3.2s | Single request returns everything |
| Single iteration page + features | ~3.2s | ~3.3s | Full feature details per iteration |
| Flow | Avg Latency | Notes |
|---|---|---|
| Dashboard (sequential: me → products → releases) | ~860ms | 3 serial REST calls |
Dashboard (parallel: Promise.all) |
~640ms | 26% faster |
| Sprint planning — REST (releases → features → detail) | ~1.7s | 3 sequential REST calls |
| Sprint planning — GraphQL (single iteration query) | ~220ms | 87% faster than REST; 1 call |
| Estimation flow (releases → unestimated features) | ~1.6s | REST, client-side filter |
- REST v1 can't filter features by iteration. The
?iteration=parameter is silently ignored. Features have noiterationfield. The only way to get iteration→feature linkage is through the GraphQL v2 API. - No batch/bulk API. Aha! has no endpoint to fetch multiple resources in one call. Every feature, release, or user list is a separate HTTP request.
- No webhooks or push. There's no way to subscribe to changes. We must poll or rely on cache TTLs.
- ~200-400ms base latency per call. Even minimal payloads take 200-400ms. This is Aha!'s server-side processing time.
- Sequential pagination (REST). Large releases with 1000+ features will always be slow on first load.
/project_teamsunavailable on some plans. The teams endpoint returns 404 on certain Aha! subscription tiers.- Field filtering helps but has limits. Using
?fields=cuts response size 30-60%, but the API still processes the full query server-side.
| Strategy | Impact |
|---|---|
| GraphQL v2 for iterations | 87% faster sprint planning; only way to get iteration→feature data |
Composite server endpoints (/api/aha/iteration-data, etc.) |
Collapse 3-5 client round-trips into 1 |
Promise.all parallel fan-out |
26% faster than sequential for dashboard |
Lean ?fields= on every REST call |
30-60% response size reduction |
| Stale-while-revalidate caching | Instant loads on revisit, background refresh |
| Tiered cache TTLs (teams 10min, iterations 2min, features 1min) | Match cache duration to data volatility |
| Request deduplication | Concurrent identical GETs share one in-flight request |
| Optimistic UI updates | Score changes, standups, days-off appear instantly |
| Hover prefetch + cache warming | Data ready before user clicks |
| Server-side pagination | Only fetch 200 features at a time for UI |
# Vitest micro-benchmarks (cache, rate limiter, pagination, points)
npm run bench
# Live API latency benchmark (REST + GraphQL, requires .env.local)
npx tsx benchmarks/live-api-benchmark.tsaha-smt.config.ts # Org config (gitignored — copy from .example.ts)
aha-smt.config.example.ts # Default config template (committed)
src/
├── app/ # Pages and API route handlers
│ ├── api/
│ │ ├── aha/ # Aha proxy routes (features, releases, iterations, etc.)
│ │ ├── standups/ # Standup CRUD
│ │ ├── sprint-snapshots/ # Snapshot capture and retrieval
│ │ ├── blockers/ # Blocker tracking
│ │ ├── action-items/ # Action item tracking
│ │ ├── days-off/ # PTO and holidays
│ │ ├── estimation-history/ # Estimation audit trail
│ │ └── settings/ # App settings
│ ├── backlog/ # Backlog grooming page
│ ├── estimate/ # Estimation flow page
│ ├── sprint/ # Sprint list + detail pages
│ ├── standup/ # Standup entry + history pages
│ ├── metrics/ # Velocity and performance page
│ └── settings/ # Settings page
├── components/
│ ├── ui/ # Base UI components (button, card, table, etc.)
│ ├── layout/ # App shell, sidebar, header
│ ├── backlog/ # Backlog table and filters
│ ├── estimate/ # Estimation queue, criteria scorer, point picker
│ ├── sprint/ # Capacity bars, allocation table, days off
│ ├── metrics/ # Velocity chart, KPI cards, comparisons
│ ├── standup/ # Standup form, timeline, blocker tracker
│ └── shared/ # Feature badge, avatar, data table, empty state
├── hooks/ # React Query hooks for all data fetching
├── lib/
│ ├── config.ts # Org config types, defaults, defineConfig(), getConfig()
│ ├── aha-client.ts # Aha REST + GraphQL client with caching and rate limiting
│ ├── aha-cache.ts # In-memory cache with stale-while-revalidate
│ ├── aha-rate-limiter.ts # Token bucket rate limiter
│ ├── points.ts # Config-driven point extraction and formatting
│ ├── constants.ts # Estimation matrix lookup, point scale, nav items
│ ├── capacity.ts # Sprint capacity calculations
│ ├── standup-parsers.ts # Standup message parsing utilities
│ ├── aha-types.ts # Aha API type definitions
│ ├── env.ts # Environment variable validation (Zod)
│ └── db/ # Drizzle schema and SQLite setup
└── providers/ # TanStack Query provider
npm test # Run all 249 tests
npm run test:watch # Watch mode for developmentTests use Vitest with vite-tsconfig-paths for @/ alias resolution. Key patterns:
- Singleton modules (
aha-cache,aha-rate-limiter,env,config) expose__reset*()functions for test isolation, called in a globalbeforeEachviavitest.setup.ts - In-memory SQLite test DB factory at
src/lib/db/__tests__/test-db.tsfor route handler tests - Rate limiter tests using fake timers call
__resetRateLimiter()aftervi.useFakeTimers() - Cache tests pass explicit TTL to
setInCache()(env mock doesn't reliably intercept relative imports)
MIT