Skip to content

Edge harvesters

Some signal can only be read from the device it lives on — Granola meeting transcripts, for instance, sit in ~/Library/Application Support/Granola/cache-v3.json on whichever Mac is running Granola. The host can’t reach them over the network.

The edge harvester pattern solves this by inverting the data flow: the device pushes to the host, authenticated with a per-device bearer token.

A workspace can register multiple devices per edge connector. Each gets its own bearer token. Pairing happens once:

Desktop Host
│ │
│ 1. POST /api/v1/harvester-devices/ │
│ pairing-codes │
│ { connector, deviceName } │
│ ──────────────────────────────────────► │
│ │
│ ◄────────────────────────────────────── │
│ { code: "ABCD-1234" } │
│ │
│ 2. User reads the code aloud / types it │
│ on the host's terminal │
│ │
│ 3. POST /api/v1/harvester-devices/claim │
│ { code: "ABCD-1234" } │
│ ──────────────────────────────────────► │
│ │
│ ◄────────────────────────────────────── │
│ { token: "<plaintext, only sent │
│ once>" } │
│ │
│ 4. Push data with Authorization: Bearer │
│ POST /api/v1/granola/sync │
│ ──────────────────────────────────────► │

The harvester_devices table stores only the sha256 of each device’s push token. The plaintext is returned at claim time and never persisted. If a device is lost, you can soft-revoke via DELETE /api/v1/harvester-devices/:id (sets revoked_at); the next push from that device gets 401.

Every push route calls authenticateHarvesterDevice(request, connectorName) which does a timing-safe sha256 comparison against every non-revoked row for the workspace + connector. The check is constant-time so probing for valid tokens via timing isn’t viable.

The HARVESTER_DEVICE_AUTH_REQUIRED env flag (default off) gates strict enforcement:

  • Off: if no device has ever been paired for this workspace/connector, unauthenticated pushes are accepted with a deprecation warning. Lets a fresh install run Granola without going through pairing first.
  • On: any push without a valid bearer token gets 401, regardless of pairing history.

Flip to on once all your devices have completed pairing.

Connectors with authType: "edge" MUST populate edgeHarvesterDocs in their manifest:

export const manifest: ConnectorManifest = {
name: "granola",
displayName: "Granola",
description: "Sync meeting notes from the Granola macOS app.",
tier: "core",
authType: "edge",
edgeHarvesterDocs: {
setupSummary: "Run Granola on a Mac and pair the device through the Desktop UI.",
pushEndpoint: "/api/v1/granola/sync",
pushIntervalLabel: "every 15 minutes",
},
filterSchema: { /* … */ },
};

The Admin SPA’s Edge Harvesters tab and the Desktop pairing UI render from these fields — there’s no hardcoded per-connector UI.

See the edge harvesters spec in the repo for the complete protocol including pairing-code TTL, error envelopes, and the full revocation flow.