Openship GitHub Webhooks Push-to-Deploy: Schema, Verification, and Pipeline Flow

Openship automates push-to-deploy by storing per-project GitHub webhook credentials in its database schema, verifying inbound deliveries via HMAC signatures, and triggering a deployment pipeline whenever a push event is received.

The oblien/openship platform implements push-to-deploy through native GitHub webhook integration, as highlighted in the project README. By persisting webhook IDs, secrets, and domains directly in the project schema, Openship can securely receive repository events and initiate builds without manual intervention. This article breaks down the database schema, verification logic, and deployment flow based on the actual source code.

Per-Project Webhook Configuration in the Database

Openship stores every project's GitHub webhook metadata in the project table. According to the source code in packages/db/src/schema/project.ts, the schema defines three critical columns for webhook operations:

// packages/db/src/schema/project.ts (lines 319-326)
webhookId: integer("webhook_id"),
webhookDomain: text("webhook_domain"),
webhookSecret: text("webhook_secret"),

These columns map directly to the GitHub API webhook object. The webhookId records the registered hook ID returned by GitHub, webhookDomain hosts the public receiver endpoint, and webhookSecret stores the shared HMAC key used to sign payloads. You can view the exact schema definition in the repository at packages/db/src/schema/project.ts.

Registering a Webhook via the GitHub API

When a project is linked to a repository, Openship generates a unique secret and POSTs a new webhook to the GitHub API. The registration payload points to the project's public domain under /api/webhooks/github:

import fetch from 'node-fetch';

const webhookUrl = `https://${webhookDomain}/api/webhooks/github`;

await fetch(`https://api.github.com/repos/${owner}/${repo}/hooks`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${GITHUB_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    name: 'web',
    active: true,
    events: ['push'],
    config: {
      url: webhookUrl,
      secret: webhookSecret,
      content_type: 'json',
    },
  }),
});

The webhookSecret generated here is persisted alongside the returned webhookId so subsequent deliveries can be authenticated.

Inbound Delivery Schema and Idempotency

All incoming GitHub webhook deliveries are recorded in the webhook_delivery table. As implemented in packages/db/src/schema/webhook-delivery.ts, this schema prevents duplicate processing by tracking the GitHub hook and delivery identifiers:

// packages/db/src/schema/webhook-delivery.ts (lines 20-21)
hookId: integer("hook_id"),
webhookId: integer("webhook_id"),
uniqueIndex("uq_webhook_delivery_github_delivery")

The composite unique index on hookId and the GitHub delivery ID ensures that retries or redelivered payloads do not spawn multiple deployments. You can inspect the schema at packages/db/src/schema/webhook-delivery.ts.

Webhook Verification and Deployment Trigger

The HTTP handler mounted at POST /api/webhooks/github performs two tasks before triggering a deployment: HMAC signature verification and delivery persistence. The handler extracts the X-GitHub-Delivery header and the X-Hub-Signature-256 signature to validate the payload against the project's stored secret.

Signature Verification Logic

import { verifyGitHubSignature } from './security';
import { recordDelivery } from './db';

export async function githubWebhookHandler(req, res) {
  const signature = req.headers['x-hub-signature-256'];
  const payload = await req.text();

  if (!verifyGitHubSignature(signature, payload, project.webhookSecret)) {
    return res.status(401).send('Invalid signature');
  }

  const deliveryId = req.headers['x-github-delivery'];
  await recordDelivery({
    projectId: project.id,
    hookId: project.webhookId,
    githubDeliveryId: deliveryId,
    payload: JSON.parse(payload),
  });

  await triggerDeploy(project.id, { source: 'webhook' });
  res.status(200).send('OK');
}

After verification succeeds, recordDelivery inserts the event into the webhook_delivery table. If the unique constraint is violated because the delivery was already processed, the pipeline aborts early. Otherwise, triggerDeploy enqueues the project's build-detect-run pipeline and marks the deployment source as webhook.

Push-to-Deploy Pipeline Flow

Once the webhook handler validates and records the event, Openship's deployment engine takes over. The pipeline re-detects the repository structure, rebuilds the application artifact, and redeploys the updated container or binary. The edge router then rewrites the virtual host to point to the new build and provisions a fresh TLS certificate if necessary. Because the deployment is triggered directly by the recorded webhook event, the dashboard reflects the new version immediately after the pipeline completes.

Summary

  • Project-level webhook storage: packages/db/src/schema/project.ts stores webhookId, webhookDomain, and webhookSecret for each connected repository.
  • Delivery idempotency: packages/db/src/schema/webhook-delivery.ts uses a unique composite index on hookId and the GitHub delivery ID to deduplicate inbound events.
  • HMAC verification: The inbound handler at POST /api/webhooks/github verifies X-Hub-Signature-256 against the stored secret before recording the delivery.
  • Automatic deployment: Valid push events trigger the build pipeline, redeploy the project, and update the edge routing configuration without manual steps.

Frequently Asked Questions

How does Openship verify that a GitHub webhook payload is authentic?

Openship retrieves the per-project webhookSecret from packages/db/src/schema/project.ts and validates the X-Hub-Signature-256 header using an HMAC-SHA256 comparison. If the computed signature does not match the header, the handler returns a 401 response and never triggers a deployment.

What prevents the same GitHub push from deploying multiple times?

The webhook_delivery schema in packages/db/src/schema/webhook-delivery.ts enforces a unique index combining hookId and the GitHub delivery ID. When a duplicate request arrives, the database insertion fails, and the pipeline exits before rebuilding the project.

Where does Openship store the GitHub webhook URL and secret?

The project table defined in packages/db/src/schema/project.ts persists the webhook ID, domain, and secret in the webhookId, webhookDomain, and webhookSecret columns. These fields are populated during the initial repository linking step.

Which GitHub events does Openship listen to for push-to-deploy?

The registration payload in the webhook setup targets the push event. When GitHub transmits a push payload to the project's /api/webhooks/github endpoint, the verified event triggers the standard Openship deployment pipeline.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →