Security

Last updated 30 September 2026

Written for the security team deciding whether MergeBell can be used with your GitLab. If something here is not specific enough, email us and we will make it so.

In one paragraph

MergeBell is a native iPhone app that talks to your GitLab instance directly. The token you sign in with is stored in the iPhone Keychain and never leaves the device. There is no MergeBell account and no server between the phone and GitLab. The only server-side component is an optional alert relay, which holds a separate, read-only key, can call only the eight GET endpoints listed below, and runs on a machine we administer in Germany.

On the phone

  • Sign-in: gitlab.com uses OAuth 2.0 with PKCE inASWebAuthenticationSession — GitLab’s own page, never an embedded web view. A self-managed instance uses a personal access token the user creates. Both need theapi scope, because the app approves, merges and comments.
  • Storage: tokens live in the Keychain withkSecAttrAccessibleAfterFirstUnlockThisDeviceOnly: excluded from backups, never synced to another device. Short-lived OAuth access tokens are refreshed single-flight, and the rotated refresh token is saved before the new access token is used.
  • Transport: HTTPS through the iOS networking stack. A certificate iOS cannot verify is explained to the user and never bypassed — there is no “trust anyway” option.
  • Cache: a local SQLite cache of lists and the last 20 opened merge requests, inside the app’s sandbox. Job logs are never cached. Signing out of an instance wipes its token, cache, queued actions and alert key.
  • No third-party code sees GitLab data. Analytics events carry no names, URLs, project or merge request identifiers; crash reports carry no GitLab content.

The alert relay

Push alerts need something to check GitLab while the phone sleeps. That is the relay atrelay.mergebell.app: our own software on a Hetzner virtual server in Germany, with a Postgres database that has no public address. Alerts are off until the user turns them on.

  • A second key, read-only by construction. Turning on alerts asks for a separate grant — OAuth with the read_api scope on gitlab.com, or a read_apipersonal access token. The relay checks the scopes and refuses any key that goes beyondread_api / read_user. The phone’s own token is never sent.
  • Encrypted at rest. Keys are sealed with libsodium before they are written; the sealing key lives in an owner-only file on the server, not in the database.
  • A hard-coded GET allowlist. Even with a mistakenly broad key, the relay’s GitLab client can only make these requests:
Endpoint (GET)Why
/api/v4/userConfirms the key belongs to the GitLab user who turned alerts on
/api/v4/versionChecks the instance is reachable before alerts are switched on
/api/v4/personal_access_tokens/selfChecks a token’s scopes, so a write-capable token is refused
/api/v4/todosFinds new pending To-Dos — the core of every alert
/api/v4/projects/:idNames the project in a notification and proves you can read it
/api/v4/projects/:id/pipelinesPipeline watches: new pipelines on a branch you chose
/api/v4/projects/:id/pipelines/:idThe status of a watched or followed pipeline
/api/v4/projects/:id/pipelines/:id/jobsWhich job failed, or is waiting to be played
  • Rate-aware. The relay spends at most about 15% of a user’s hourly GitLab allowance, reads a watched branch once per cycle however many people watch it, and backs off on429 using Retry-After.
  • Webhooks are a doorbell, not a data feed. Instant mode (optional, Maintainer or Owner) adds a project webhook pointing at the relay with a secret token and SSL verification on. The relay checks the secret in constant time, discards the payload unread, and runs the same read-only poll a few seconds later. The secret is stored as a hash.
  • Minimal notification content. Text is composed in memory from the To-Do and cleared once delivered; stored alert records keep ids and the event kind, not titles.
  • Unreachable instances stay unreachable. If the relay cannot reach an instance (for example behind a VPN), that instance switches to on-device checks. Nothing is tunnelled or exposed.

Retention and deletion

  • Alert records are kept 14 days.
  • Settings → Delete alert data removes the webhooks the app installed and deletes everything the relay holds for that user in one transaction.
  • A key GitLab reports as revoked is wiped on the next failed read, and never retried.
  • A subscription with no reachable device for 7 days is deleted with its keys.

Revoking the key in GitLab — User settings → Applications, or Access tokens — stops the relay immediately, whatever state the app is in. More on the deletion page.

For network and GitLab administrators

  • The phone connects only to your GitLab instance, plus Apple, RevenueCat and Firebase for notifications, subscriptions, analytics and crash reports.
  • If you use alerts, the relay connects to your instance from relay.mergebell.app. Instant-mode webhooks call https://relay.mergebell.app/v1/webhooks/gitlab/….
  • The gitlab.com OAuth application is a public PKCE client owned by MergeBell; it has no client secret to leak.

Reporting a vulnerability

Email [email protected] with the subjectSecurity. We reply within two working days and credit reporters who want credit. Please don’t test against other people’s accounts or instances.