Labels
View on GitHubLabels
Issue labels are a per-project resource backed by a real
labels database table (App\Models\Label) — not a fixed backend
enum. Every project gets a starter taxonomy of six labels (bug,
feature, performance, design, ux, chore) seeded lazily the
first time they're needed, and project owners/admins can rename,
recolor, describe, add, or delete any label — including the seeded
ones — from Settings → Labels.
Guides, in the order you'd actually need them
- Add a new default labelLabelsAdd a new default labelWorked example: adding a seventh starter label, security, to the six every project is seeded with (bug, feature, performance, design, ux, chore). This only… —
worked example adding a seventh starter label,
security, to the taxonomy every new project is seeded with. - Expose project labels to a new pageLabelsExpose project labels to a new pageWorked example: giving a brand-new, single-project page real label colors (instead of useProjectLabels()'s hashed fallback) by mounting a ProjectLabelsProv… —
worked example wiring real per-project label colors (instead of the
hashed fallback) into a brand-new single-project page, using a
hypothetical
Backlogpage in the exact shape of the existingProjects/Showpage.
The architecture in one paragraph
labels (migration 2026_09_12_120000_create_labels_table.php) has
project_id, name, color, description, is_system, unique on
(project_id, name). App\Repositories\LabelRepository and
App\Services\LabelService follow this codebase's usual
Controller → Service → Repository split (see
../architecture/02-backend-layered-architecture.mdArchitecture & project technicalsBackend layered architectureController → Service → Repository, with no exceptions and no shortcuts — a controller never builds an Eloquent query, and a repository never contains busin…):
LabelService::ensureSystemLabels() seeds the six starter labels
exactly once per project, tracked by a projects.labels_seeded_at
timestamp — not "insert whatever's missing" on every read, which would
silently resurrect a label a project owner deliberately deleted.
App\Http\Controllers\LabelController exposes
projects.labels.store / .update / .destroy, gated by
App\Policies\ProjectPolicy::viewLabels() / manageLabels() (view:
owner/admin/member/viewer; manage: owner/admin — the same
owner/admin-only pattern as viewIntegrations()/updateIntegrations()).
An issue's labels column is still a plain JSON array of label
name strings (Issue::casts()['labels'] = 'array', no more
App\Enums\IssueLabel backing it) — but every name written through
IssueController@store/@update is now validated against that
project's real labels rows
(Rule::exists('labels', 'name')->where('project_id', ...)) instead of
a fixed enum. That's a deliberate scope tradeoff worth knowing before
you touch it: a label is identified by name, not by a relational
pivot to issues, so renaming or deleting a label does not
retroactively update or strip it from issues that already reference
the old name — an issue keeps whatever string it was tagged with until
someone edits its labels again. On the frontend,
resources/js/context/ProjectLabelsContext.tsx's
ProjectLabelsProvider/useProjectLabels() is the single source of
truth for "what labels exist and what color is this one" wherever it's
mounted (currently Pages/Projects/Show.tsx and Pages/Issues/Show.tsx,
each fed by a labels prop their controller builds with a small
private mapLabels() helper) — LabelBadge, EditableLabelList, and
the labels FilterDropdown all read from it instead of a hardcoded
list, and useProjectLabels().getColor(name) falls back to
hashLabelColor() (a deterministic hash into LABEL_COLOR_PALETTE in
resources/js/utils/labelColors.ts) when no provider is mounted or the
name isn't found — which is exactly what happens today on the
cross-project Dashboard page. The Settings → Labels tab itself
(WorkspaceSettingsLabelsTab.tsx) was unlocked from
documentation/en/settings-tabs/01-flip-a-placeholder-tab-live.md's
"coming soon" state and rebuilt with real CRUD wired to the routes
above; its create/edit UI is a panel that expands inline in place
rather than a modal — see that file's WorkspaceSettingsLabelInlineEditor.tsx
if you're adding another editable-in-place settings row and want to
match the pattern instead of reaching for a Modal.
