Integrations
View on GitHubThe GitHub integration
Unlike the notify (webhook) and import (pull) integrations covered in the other guides in this category, GitHub is a third kind: a GitHub App installation, connected through a small public relay service, that links a pull request to an Orbit issue. This guide documents the actual feature — architecture, how to connect a project, the marker syntax, and troubleshooting — rather than "how to add another one", since GitHub is the only integration of this kind.
Why a relay is needed
Orbit is self-hosted, so a typical Orbit Local instance isn't publicly
reachable — GitHub can't deliver a webhook straight to it. orbit-api
(a small, separately-hosted Netlify Functions service, api.orbit-dev.app
by default) exists to bridge that gap:
GitHub --webhook--> orbit-api <--poll-- Orbit Local --comment request--> orbit-api --GitHub App API--> GitHub
- GitHub sends a
pull_requestwebhook to orbit-api when a PR is opened. - orbit-api verifies the webhook signature and stores it as a pending "relay event", scoped to the project's connection.
- Orbit Local polls orbit-api once a minute (
PollGithubRelayEvents, scheduled inroutes/console.php) for pending events. - For each event, Orbit Local parses the PR body for an
<!-- orbit-issue:ID -->marker, resolves the issue, and persists the link. - Orbit Local asks orbit-api to post a confirmation comment on the PR, using orbit-api's GitHub App installation token — Orbit Local never holds a GitHub token or the GitHub App's private key itself.
- Orbit Local acknowledges the event, which removes it from the pending queue.
orbit-api's own source is the authority for its API contract; this guide only covers the Orbit Local side.
Local storage
There is no dedicated table for the GitHub connection — it extends the
same project_integrations row every other integration uses (see
guide 05IntegrationsAdd an import integrationWorked example: turning Linear from a locked "coming soon" catalog card into a fully working import integration, the same way Jira works today. This is the…'s "never add
per-integration schema" rule), with GitHub-specific columns
(github_status, github_relay_token, github_installation_id,
github_repository_owner/github_repository_name, etc.) added by a
migration. github_relay_token is encrypted at rest ('encrypted'
cast on App\Models\ProjectIntegration) and is never included in any
Inertia prop — the settings page only ever receives a small status DTO
(GithubIntegrationService::getConnectStatus()).
The PR ↔ issue link itself reuses external_issue_links (the same
table Jira's import integration uses for dedup), tagged with
external_type = 'github_pull_request'. Its unique constraint on
(project_integration_id, external_id) — where external_id is
GitHub's own numeric pull request id — is what makes relinking the
same PR (e.g. on a retried relay event) idempotent instead of creating
a duplicate row.
Environment
ORBIT_API_URL=https://api.orbit-dev.app
Set in .env/.env.example and read via config('services.orbit_api.url')
(config/services.php) — never hardcode the URL in a service. Point
this at a self-hosted orbit-api instance if you're not using the
default one.
Connecting a project
- Project Settings → Integrations → GitHub → Connect with GitHub.
- Orbit Local asks orbit-api to create a connection
(
GithubIntegrationService::connect()), stores the connection id and relay token (encrypted), and opens the returned GitHub App install URL in a new tab. - The settings page polls (
WorkspaceSettingsIntegrationsTab, every ~2 seconds, giving up after 5 minutes) while the connection ispending. - Once the GitHub App installation completes and exactly one
repository has been selected, orbit-api marks the connection
connected; the next poll picks that up and the UI shows the connected repository.
Disconnect revokes the connection with orbit-api and marks the
local row revoked — it does not uninstall the GitHub App from the
GitHub organization (see limitations).
Marker syntax
The only supported way to link a pull request to an Orbit issue is a hidden marker in the PR's description:
<!-- orbit-issue:213769 -->
- No marker → the event is ignored (acknowledged, not retried).
- Exactly one marker → that issue is resolved and linked, as long as it belongs to the same Orbit project the GitHub integration is connected to. An issue from a different project is never linked.
- Two or more markers → treated as ambiguous. Nothing is linked, and the ambiguity is logged for diagnostics. This is a permanent outcome, not a transient failure — the event is still acknowledged.
There is no fallback: no title, branch, or commit-message parsing.
See App\Services\Integrations\Github\GithubMarkerParser.
Acknowledgement semantics
An event is acknowledged (removed from the pending queue) once
GithubRelayEventProcessor::process() returns without throwing — this
covers both a successful link and every permanently-invalid case
above (no marker, ambiguous, issue missing, wrong project, unsupported
event/action). A transient failure — orbit-api unreachable, the
comment request failing, a database error — makes process() throw,
and PollGithubRelayEvents deliberately does not acknowledge the
event in that case: it stays pending and is retried on the next poll.
There is no separate retry-queue infrastructure; the relay's own
pending state is the retry mechanism.
MVP limitations
- Only
pull_request.openedis handled — edits, closes, merges, reviews, and CI/check runs are not synced. - One GitHub connection per Orbit project, one repository per connection, and one Orbit issue per pull request.
- No status automation: linking a PR never transitions the issue's status or closes it.
- No comment sync in either direction beyond the single confirmation comment orbit-api posts once.
- Disconnecting in Orbit does not uninstall the GitHub App from GitHub — it only stops Orbit Local from trusting that connection.
Troubleshooting
Connection stuck on "pending" — the GitHub App installation was never completed, or completed with zero or more than one repository selected (orbit-api requires exactly one). Reopen the install page from the integration panel and try again.
Connection shows "revoked" and won't reconnect — click Connect with GitHub again; this always requests a brand-new orbit-api connection rather than trying to resume the old one.
A PR isn't getting linked — check the marker is exactly
<!-- orbit-issue:ID --> with a numeric id, appears exactly once in
the PR description, and that the issue id belongs to the same Orbit
project as the connected repository. Also confirm the connection's
status is connected, not pending.
Events not processing — confirm the scheduler is actually running
(composer dev's queue/schedule processes, or your production cron
entry for php artisan schedule:run); PollGithubRelayEvents only
runs once a minute if something is invoking Laravel's scheduler.
Security notes
- Orbit Local only ever holds the orbit-api relay token — it never receives or stores a GitHub token or the GitHub App's private key. Those secrets live entirely in orbit-api.
- Orbit Local never calls the GitHub REST API directly; all GitHub-authenticated actions (posting the confirmation comment) are proxied through orbit-api using its own GitHub App installation token.
github_relay_tokenis encrypted at rest and is never logged or sent to the frontend.
