Nook's GitHub integration connects project thinking in Nook with work happening in GitHub.
The current integration is intentionally one-way:
GitHub repository
↓
Nook workspace
↓
GitHub issue
↓
Nook artifact
The integration is implemented as an asynchronous event-driven pipeline so webhook processing is decoupled from the HTTP request lifecycle.
GitHub
│
│ HTTPS webhook
▼
Nook API
│
│ HMAC verification
│
│ publish event
▼
RabbitMQ
│
│ consume
▼
GitHub Worker
│
│ Prisma
▼
PostgreSQL / Neon
│
▼
Nook Artifact
The API is responsible for:
- webhook authentication
- request parsing
- event publication
The worker is responsible for:
- asynchronous event processing
- repository/workspace resolution
- GitHub issue synchronization
- idempotency
- persistence
- retries
- dead-letter handling
POST /webhooks/github
The GitHub webhook route is registered before the application's normal JSON parser:
app.use(
"/webhooks/github",
express.raw({ type: "application/json" }),
githubWebhookRouter,
);
app.use(express.json());The raw request body is required because GitHub calculates the webhook signature from the exact request payload.
The webhook route reads:
X-GitHub-Delivery
X-GitHub-Event
X-GitHub-Signature-256
GitHub webhook requests are authenticated using:
HMAC-SHA256
with:
GITHUB_WEBHOOK_SECRET
The calculated signature is compared using a timing-safe comparison.
Invalid or missing signatures are rejected with:
401 Unauthorized
The API does not enqueue unauthenticated events.
After signature verification, the API publishes the event to RabbitMQ.
The API does not synchronize the GitHub issue synchronously.
Webhook request
↓
Verify signature
↓
Publish event
↓
Return HTTP 200
This keeps the webhook request independent of database synchronization latency.
RabbitMQ provides the asynchronous boundary between webhook ingestion and GitHub synchronization.
The following queues are used:
github-events
github-events-retry-1
github-events-retry-2
github-events-retry-3
github-events-dlq
github-events
│
│ processing failure
▼
retry-1
5 seconds
│
▼
github-events
│
│ failure
▼
retry-2
10 seconds
│
▼
github-events
│
│ failure
▼
retry-3
20 seconds
│
▼
github-events
│
│ final failure
▼
DLQ
Retry queues use RabbitMQ message TTL and dead-letter routing.
Messages are acknowledged only after successful processing.
consume
↓
process
↓
database transaction succeeds
↓
ACK
If processing fails, the event enters the retry/DLQ path.
If publishing to a retry queue or the DLQ fails, the original message is not acknowledged so RabbitMQ can redeliver it.
The GitHub worker initializes the RabbitMQ topology before consuming messages:
await setupGithubQueue();
const channel = await getRabbitChannel();This means the worker does not depend on another application process having created the queues first.
The startup flow is:
Worker starts
↓
setupGithubQueue()
↓
Queue topology asserted
↓
RabbitMQ channel created
↓
github-events consumed
GitHub webhook deliveries are not assumed to be exactly-once.
The X-GitHub-Delivery value is stored as a unique deliveryId.
model WebhookDelivery {
id Int @id @default(autoincrement())
deliveryId String @unique
event String
status String @default("RECEIVED")
receivedAt DateTime @default(now())
processedAt DateTime?
}The worker checks the delivery ID before processing.
Receive event
↓
Check deliveryId
↓
Already processed?
/ \
yes no
↓ ↓
skip process
This protects the system from duplicate webhook deliveries.
GitHub synchronization and webhook-delivery persistence are performed within the same Prisma transaction.
Conceptually:
BEGIN
↓
Check deliveryId
↓
Synchronize GitHub issue
↓
Record WebhookDelivery
↓
COMMIT
↓
ACK RabbitMQ message
If the transaction fails, the database changes are rolled back and the message remains eligible for retry.
A GitHub repository is mapped to a Nook workspace through GithubRepository.
model GithubRepository {
id Int @id @default(autoincrement())
owner String
name String
userId Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
workspaceId Int
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
@@unique([owner, name])
@@index([workspaceId])
}The mapping flow is:
GitHub owner/name
↓
GithubRepository
↓
workspaceId
↓
Nook artifact
GitHub issue synchronization is implemented in:
backend/src/services/github-sync.ts
The worker currently handles:
issues
events for:
opened
edited
closed
reopened
Pull requests are ignored even when delivered through the GitHub issues event family.
if (payload.issue?.pull_request) return null;Two failures appeared together during production debugging:
GET /workspaces/:workspaceId/artifactsreturned500after GitHub issues had been stored. The active Cloud Run revision used an image whose generated Prisma client did not recognizeGITHUB_ISSUE, so Prisma failed while reading artifacts. Updating the frontend API URL could not fix this server-side enum mismatch. Recovery required routing traffic to a revision built withGITHUB_ISSUEsupport.- After the API returned artifacts, cards still appeared missing from the canvas. GitHub sync had omitted
xandy; Prisma schema defaults stored both as0. Separately, initial framing used minimum and maximum coordinates across every artifact. Distant saved positions pulled the camera between spatial groups. Rendering and logical-to-render coordinate conversion were working; cards were outside the clipped viewport.
The 500 was resolved separately by routing traffic to an enum-aware revision. PR #28 addresses canvas placement and framing: new GitHub issues receive deterministic, non-overlapping positions near the largest spatial cluster; GitHub updates preserve existing positions; initial framing measures card sizes, finds the main cluster by spatial proximity, and fits its bounds with padding while capping initial zoom at 1. Distant artifacts remain rendered and reachable by pan and zoom. Workspace loading changes camera state only; it does not rewrite saved coordinates.
Existing GitHub artifacts at (0, 0) may have received those coordinates from old Prisma defaults. Moving them requires a separate, reviewed, narrowly scoped, idempotent data repair. PR #28 does not migrate existing records; production data was not changed as part of this fix.
A GitHub issue is identified using:
owner/name#issueNumber
For example:
Dunkrick/webhook-test#6
The artifact stores:
source = github
externalId = Dunkrick/webhook-test#6
The artifact uses an upsert so subsequent GitHub events update the existing artifact.
GitHub issue #6
↓
Dunkrick/webhook-test#6
↓
existing Artifact?
/ \
yes no
↓ ↓
update create
This prevents edits, closes, and reopens from creating duplicate Nook artifacts.
GitHub issue information is stored in the artifact's JSON content:
{
"source": "github",
"repository": "Dunkrick/webhook-test",
"issueNumber": 6,
"title": "Production E2E Test - Updated",
"body": "Testing GitHub → Nook production infrastructure.",
"state": "open",
"url": "https://github.com/Dunkrick/webhook-test/issues/6",
"author": "Dunkrick"
}The synchronized artifact uses:
type = GITHUB_ISSUE
The production integration has been validated across:
opened
↓
edited
↓
closed
↓
reopened
All events for the same GitHub issue updated the same Nook artifact.
Example:
GitHub issue
Dunkrick/webhook-test#6
↓
Nook Artifact #66
The artifact remained:
id = 66
externalId = Dunkrick/webhook-test#6
while its content state changed:
open
↓
closed
↓
open
The production runtime is:
GitHub
↓
Cloud Run API
↓
CloudAMQP / RabbitMQ
↓
Cloud Run Worker Pool
↓
Neon PostgreSQL
The API runs as:
Cloud Run service: nook-backend
Region: asia-south1
The worker runs as:
Cloud Run Worker Pool: nook-github-worker
Region: asia-south1
Both runtimes use the same immutable container image.
Runtime secrets are stored in Google Secret Manager:
nook-database-url
nook-jwt-secret
nook-rabbitmq-url
nook-github-webhook-secret
Non-secret configuration remains environment configuration:
FRONTEND_URL
GCS_BUCKET_NAME
Secrets are injected into both the API and worker runtime.
The integration distinguishes the infrastructure flow into:
HTTP authentication failure
↓
reject request
RabbitMQ processing failure
↓
retry
Repeated processing failure
↓
DLQ
Successful database transaction
↓
ACK
A future refinement should distinguish transient failures from permanent/domain failures so that errors such as an unmapped repository do not unnecessarily consume all retry attempts.
The current implementation is intentionally GitHub → Nook.
Planned evolution:
V1
GitHub → Nook synchronization
V2
Nook → GitHub issue creation
V3
Bidirectional state synchronization
V4
Ideas → Issues → PRs → Shipped
The GitHub integration is infrastructure supporting Nook's broader project-thinking workflow rather than the product endpoint itself.