Open-source issue tracking: issues, sprints, projects, goals and roadmap.
An open alternative to Linear and Jira.
Features · Install · How it works · Development · Design brief
An issue tracker for product teams with its own point of view: paper, ink and a highlighter; a real ledger instead of a feed; issues that open beside the list instead of replacing it; an intake that asks for one decision at a time.
This is a Zite solution, meaning a workspace you install into your own Zite account and then edit. Zite provides the Postgres database, the endpoint runtime, auth and hosting. Everything above that is the ~43,000 lines of TypeScript in this repository.
Everything a team needs is here: intake, issues, sprints with burndowns, projects with milestones and check-ins, goals, a roadmap, saved views, an inbox, reports, settings with roles, and six AI assists that degrade gracefully when no AI is attached.
It opens on a populated demo: a fictional product team with 3 teams, 110 issues, 9 sprints and 9 projects, so every screen has something in it the first time you look. Settings has a one-click way to delete all of it.
The design language is documented in DESIGN.md, which is worth reading before changing any UI.
One top bar, not a sidebar. Home · Inbox · Issues · Sprints · Projects · Goals · Roadmap · Reports, plus search, New issue and your account.
Team scope, not team trees. A switcher next to the logo scopes Issues,
Intake, Sprints, Projects, Roadmap and Reports to one team or all of them. The
scope is the first segment of those URLs (/eng/issues, /all/sprints) and
[ / ] cycle through it.
| Surface | What it does |
|---|---|
| Home | Today: what needs you (overdue, blocked, due soon, intake waiting), what's in flight, up next, sprint pulse per team, recent inbox, pins. Tabs for everything assigned to, created by, or followed by you. |
| Inbox | A two-pane reader grouped by day. Events about one issue fold into one line. Done / unread / snooze; J K E U ⇧H. |
| Issues | The ledger: a real table with sortable column headers, group bands and cells you edit in place, or a board with drag-and-drop lanes. Tabs for all, in flight, backlog and intake, and a saved-views menu. |
| Intake | A review deck. One report at a time with who sent it, what it says and whether something similar already exists; decide with 1 accept, 2 accept and plan, 3 duplicate, 4 decline (with a reason), → skip. |
| Issue | Opens in a sheet beside any list (J/K keep walking), or as a full page. Serif title, a "ticket stub" of editable facts, Markdown description, breakdown (sub-issues, with AI suggestions), links, relations, and a discussion where runs of history fold into one line. |
| Sprints | Per team: current-sprint scoreboard with the goal as a pull quote, up next, and a history ledger with velocity. Across teams: every team's sprint side by side. Sprint page: burndown, scope added mid-sprint, risk assessment, breakdown that narrows the issue board, and Complete sprint with rollover. |
| Projects | Gallery, table or board by status. Project page with progress over time, a milestone track, facts, contributors and a check-in feed that sets health. |
| Goals | Company goals with project health and rolled-up progress; goal page with its projects and a compact timeline. |
| Roadmap | Every dated project on one timeline, grouped by goal, team or status. Drag a bar or its edges to reschedule, with Undo. |
| Reports | A masthead of headline numbers (each opens the list behind it), velocity, throughput, cycle and lead time, status mix, age, workload and breakdowns. |
| Views | Saved filters, grouping, order and layout, for yourself, a team or everyone. |
| Settings | Profile and appearance, members and roles, teams (statuses editor with drag to reorder, sprints, intake, estimates), labels and templates. |
New issue understands quick-capture tokens in its title: @name assigns,
#label labels, !high sets priority, +project files it, ^fri sets a due
date. It also offers templates, Draft with AI, and live duplicate detection.
Keyboard first. ⌘K searches issues and reaches everything else: your pins,
any team's issues, intake or current sprint, and commands for the open issue.
C new issue, / search the list, ? all shortcuts, G then H M N I
T S P O R E V to jump, and S P A L I ⇧P ⇧S ⇧D ⇧E
⇧T on issues and selections. /#/eng/sprints/current always opens that
team's running sprint, which makes it a good bookmark.
Light and dark themes, and usable on a phone.
Zite apps are built by pointing a coding agent at the platform over MCP, and installing one works the same way.
1. Connect the Zite MCP server to your agent.
claude mcp add --transport http zite https://mcp.zite.com/mcp(Cursor, VS Code and any other MCP client work the same way. See the Zite quickstart.)
2. Give it this prompt.
Install https://github.com/zite/issue-tracker into a new Zite workspace.
create_workspacenamed "Issue Tracker", thencreate_sandboxon it.- In the sandbox, add this repo as a git remote and check its files out over
/workspace, keeping the sandbox's ownzite.config.json.- Read
zite.schema.jsonand create all 22 tables withcreate_table, passing each field'sdefinition(name,type,template) straight through. Do this beforecreate_app, becausecreate_appandcheck_apprefreshzite.schema.jsonfrom the live database, and would otherwise blank it.create_app"Issue Tracker" (internal). That exact name producesapps/issue-tracker, which is what this repo already uses.- Run
yarn install, so the workspace packages are linked.check_app,commit, thenpublish_app.
3. Open the app. It seeds the demo on first load. When you are ready for real data, use Settings → Workspace → Remove demo data.
A Zite workspace is one database with one or more apps on top of it. The split that matters:
| Part | Where it runs |
|---|---|
apps/*/src/ minus api/ |
The browser. A normal Vite + React SPA. |
apps/*/src/api/*.ts |
Zite's endpoint runtime, server-side. One file = one endpoint. |
packages/* |
Imported directly. No build step; consumed as TypeScript source. |
.zite/ |
Generated clients: typed DB access and a typed caller. Never edited by hand. |
The frontend never touches the database. It calls endpoints through a generated typed
client (import { listIssues } from 'zitejs/api'), and endpoints reach the database
through another (import { zite } from 'zitejs/db'). 44 endpoints, 18 pages.
22 tables. Issues is the centre.
Teams ──< Statuses Goals ──< Projects ──< Milestones
│ └──< Sprints │ └──< Check Ins
│ └──< Labels (team or workspace) │
└──< Issues >─────────────────────────────────┘
│ ├─ assignee / creator → Members ──< Team Members
│ ├─ parent → Issues (sub-issues)
│ ├──< Issue Labels, Issue Relations, Issue Attachments, Issue Subscribers
│ ├──< Comments ──< Reactions
│ └──< Activity
Notifications, Pins, Views, Issue Templates
Foreign keys are text columns, not linked_record fields. Every list is a
filtered, sorted, grouped SQL query (findAll ignores sort), Issues points
at Members twice, and sub-issues are self-referential. Two consequences, both
handled in src/server/:
- Joins cast the uuid side:
t.id::text = i."teamId". - An unset text field is
'', neverNULL. "No assignee" isCOALESCE(col, '') = '';ref()inserver/sql.tsmaps''tonull.
The actor comes from the session, never from the request (getActor), and
roles are enforced on the server (assertCan). Settings mirrors them by
locking controls with an explanation.
| Role | Can |
|---|---|
| Admin | Everything, including inviting people, changing roles and deactivating members |
| Member | All work, plus teams, statuses, labels and templates |
| Guest | All work on issues, projects and sprints; can view settings but not change them |
Rows carry ids; the client resolves names. listIssues returns ids and
bootstrap loads every reference table once (lib/workspace.tsx), so an edit
is one optimistic cache write that re-renders the ledger, board and sheet at
once (lib/mutations.ts).
seedWorkspace runs once, on first open, and is idempotent. It builds
Quillmark, a fictional collaborative writing app: 3 teams, 10 people, 9
projects under 3 goals, 22 milestones, 14 check-ins, 9 sprints (past, current,
upcoming), 110 issues with sub-issues, relations and links, 52 comments with
replies and reactions, 429 history entries, 7 saved views, 4 templates and an
inbox. Whoever opens it first is "you": they get real work across statuses, the
inbox and pins. Dates are relative to the moment of seeding.
Through the workspace's Anthropic connection (src/server/ai.ts):
| Where | What | Without AI |
|---|---|---|
| New issue → Draft with AI | A rough note becomes a titled, described, labelled, estimated issue for review | The note becomes the title and description |
| New issue and Intake | Duplicate detection: SQL narrows by word overlap, Claude judges | Strong keyword overlap only |
| Issue → Breakdown → Suggest | A breakdown into sub-issues you pick from | Hidden |
| Issue → Discussion → Catch me up | Decisions, open questions and next steps | Hidden |
| Sprint → Assess risk | Names what puts the sprint at risk; the verdict itself is arithmetic | The computed verdict and flags |
| Project → Draft with AI | A check-in drafted from what shipped and what's in flight | A factual summary to edit |
JSON Schemas are written by hand (the SDK's zod helper needs zod v4; Zite pins
zod 3). The accepted shape is exactly { type: 'json_schema', schema }.
sendDueReminders runs daily at 08:00 UTC and notifies assignees about work due
tomorrow or newly overdue.
yarn install
cp .env.example .env.local # then put your own workspace id in it
yarn dev # :8080What works offline: the whole frontend, tsc, and vite build. Editing a
component hot-reloads.
What does not: the endpoints in src/api/ execute on Zite's runtime against your
workspace database, not on your machine. yarn dev serves the UI, but every endpoint
call goes out to the workspace named in .env.local and needs a session for that
organization. There is no local database mode yet.
Run yarn generate after adding, renaming or deleting an endpoint.
yarn run check # tsc + endpoint bundling + vite buildNote. On an app this size
zitejs checkprintsbundle endpoints ✗with no error and exits non-zero. That is a 1 MB stdout buffer in the checker, not a real failure. To see genuine endpoint errors, bundle to a file instead:npx zitejs bundle --app issue-tracker > /tmp/b.jsonand readendpointErrors.
- A new issue property: add the field, then
issueDto/ISSUE_SELECT/mapIssueRowinserver/issues.ts,PATCHABLEinapi/updateIssue.ts,TRACKEDinserver/changes.ts, a column inissues/IssueLedger.tsxand a fact inissue/Facts.tsx. - A new filter:
issueFilterSchemaandbuildIssueWhereon the server, thenFILTERSinissues/filters.tsx. - A new list surface: render
<IssuesView surfaceKey baseFilters … />; it owns fetching, grouping, sorting, selection, keyboard, bulk actions and views. - A new scoped section: add it to
SCOPED_SECTIONSinlib/scope.tsxand route it under/:scope/…inApp.tsx.
React 18 · TypeScript · Vite · Tailwind CSS 3 · shadcn/ui · Radix · TanStack Query & Table · Recharts · dnd-kit · date-fns · zod · zitejs · Claude for the optional AI assists.
Issues and pull requests are welcome. See CONTRIBUTING.md, and read DESIGN.md before changing UI. Anything security-related goes to SECURITY.md instead of a public issue.