Data model & visibility
The entity hierarchy, the visibility cascade, identifiers, and how KHUB handles deletion.
Entity hierarchy#
Organization
├─ Team ──────────────────────┐ (grants access to Projects/Repositories)
├─ Project │
│ ├─ Repository ◀───────────┘
│ │ ├─ ChatChannel (one, type=REPOSITORY, auto-created)
│ │ ├─ PullRequest, Branch, Tag, Release, WikiPage
│ │ ├─ Pipeline → PipelineJob (DAG)
│ │ ├─ Webhook, Secret (scope=REPOSITORY)
│ │ └─ RetentionPolicy target (via Organization)
│ ├─ Issue, Label, Milestone (scoped to the Project, referencing any of its Repositories)
│ └─ Secret (scope=PROJECT)
├─ ChatCategory → ChatChannel (type=ORGANIZATION)
├─ PackageVersion (npm-compatible registry, scope=ORGANIZATION)
├─ OciRepository → OciManifest → OciBlob (container registry)
├─ Secret (scope=ORGANIZATION)
└─ AuditLog, Notification, Webhook (org-level)Chat direct messages and group chats (ChatChannel with type IN (DIRECT, GROUP)) are not nested under an organization at all — membership is defined directly via ChatChannelMember rows.
Visibility cascade#
Every Organization, Project, and Repository independently carries a visibility of PRIVATE, INTERNAL, or PUBLIC. The effective visibility of a repository is the most restrictive of the three:
effectiveVisibility(orgVisibility, projectVisibility, repoVisibility) =
min(orgVisibility, projectVisibility, repoVisibility) // PRIVATE < INTERNAL < PUBLICSo a repository marked PUBLIC inside a PRIVATE organization is still private — there is no way for a child resource to widen access beyond what its parent allows. This is computed on every request in packages/permissions, not denormalized onto the repository row, so a change to an organization's visibility takes effect for every repository beneath it immediately.
Primary keys and identifiers#
Every table uses a UUID v4 primary key (@default(uuid())), generated application-side by Prisma at insert time. Slugs (Organization.slug, Project.slug, Repository.slug) are separate, human-readable, unique-per-parent identifiers used in URLs and Git remotes — they are mutable (renaming a repository changes its slug, not its ID), so anything that must survive a rename (webhook configuration, CI job references) is keyed by ID internally.
Soft state vs. hard delete#
Most destructive actions in KHUB are reversible short of an explicit delete: repositories can be archived (read-only, fully browsable) rather than deleted; user accounts are deactivated rather than deleted by default. Hard deletes cascade according to each relation's onDelete policy in packages/database/prisma/schema.prisma — for example, deleting a Repository cascades to its pull requests, pipelines, and webhooks, but deleting a User who authored chat messages or audit log entries is blocked at the database level (foreign key RESTRICT) until that content is reassigned or removed, precisely so that shared history (a channel's message log, the audit trail) can't silently lose its attribution.
