tappify extension add work, then implement the generated work handlers and provider webhook
adapter with @tappify/extension-sdk@2.1.0-beta.1.
This contract is implemented behind a disabled-by-default runtime flag. The SDK beta is published;
the later implementation’s focused/runtime suites, repository release gates, and internal staging
acceptance remain pending. The examples describe the contract, not a completed production rollout.
Review threads and customer reviews
Work destinations use the Review SDK: teammates pin feedback to a screen in the owner’s iOS or Android app and continue the conversation on the host’s Review page.review.threads:read grants the scoped thread snapshot, comments, screenshot descriptors, build
context, and safe device context. reviews:read grants App Store and Google Play customer-review
data instead. Customer reviews and public store replies are outside this work protocol.
Declare capabilities and authentication
The following fragment shows Work Bridge’s capability and scope shape. Add the usual extension metadata and a deployed server as described in the manifest reference and server guide.kind is tracker, docs, or chat; creates contains issue, page, or message.
Those capabilities describe the extension. Exact provider item types come from container discovery.
Current work manifests need review.threads:read and work:create; any comment or status direction
other than none also needs work:sync. Grants must exist on the install, not only in the manifest.
Inbound writes only affect an established link under work:sync; there is no general extension
scope for writing arbitrary Review threads.
Each install uses one provider identity. Provider writes use that connection’s credentials, even
when the content includes a teammate’s display name. identity: "user" is rejected. Work auth is
either API-key credential fields or { "method": "oauth", "provider": "provider-id", "scopes": [] }.
Provider OAuth scopes differ from Tappify scopes. Configure vendor OAuth endpoints, client
credentials, authorization/token parameters, PKCE, and resource discovery in the developer portal;
client secrets belong in the vendor vault. Install credentials remain in the host vault and arrive
on authenticated work calls. Do not put either credential set in manifests, logs, or callback records.
Comment and status directions are independent:
The owner can turn new-comment synchronization off. Comments are append-only: edits, deletions,
reactions, and comment attachments do not synchronize. Provider comments carry the extension’s
identity and the provider author’s display name; they do not impersonate a Tappify teammate or
resolve provider mentions as Tappify mentions.
The SDK v2 compatibility window still accepts complete legacy manifests with static
container,
fields.required, string-valued comments, and status.map, with migration warnings. Legacy
reviews:read validation leniency does not grant Review-thread access at runtime. Migrate to the
Review-specific scope, auth, capability objects, and dynamic metadata. Legacy status and resolve
handler routes remain deprecated; a missing set_status can fall back to resolve for resolved
targets only, with externalItemId adapted to id. Implement set_status for reopen as well.
Doctor checks auth, unsupported identity, scopes, deprecated fields, and a public HTTPS production
server.baseUrl for active sync. The CLI emits typed stubs and a webhook placeholder; it does not
implement provider delivery or durable persistence for you.
Discover and configure a destination
After connecting, the owner selects a container on the install page.list_containers supports
cursor, query, and parentId; pass opaque cursors unchanged. describe_container returns the
container, item types, create fields, and current statuses for the selected container.
Each create field has an ID, label, type (string, number, boolean, or string[]), required
flag, optional choices, and optional itemTypeIds restrictions. The host validates supplied values
against fresh metadata. Manual configuration may omit required create values; creation still
requires valid provider fields. Automatic mode requires every applicable required value before
the host accepts configuration. Both modes require a current item type and valid exact status targets.
The host sends the exact selected
providerStatusId on set_status, rather than guessing from a
label or global manifest map. A changed provider field or unavailable target needs corrected
configuration; invalid automatic configuration is rejected, and delivery validation failures can
surface as configuration_required.
In manual mode the thread drawer offers Create in for an eligible destination. In automatic
mode a newly committed thread queues creation; a failed automatic create can also be recovered
from the drawer. Existing links are returned instead of creating another item. Once linked,
comments and status follow their configured directions in either creation mode.
Accepted configuration has a configurationRevision. Callback provisioning has a generation;
secret rotation can advance the generation without changing the owner’s revision. configure
must acknowledge both exact values and return registeredWebhookIds. Fence stale generations
and revisions so a delayed configure or disconnect cannot overwrite the current registration.
The host also checks accepted configuration and queued status targets when retrying.
Implement the nine operations
Register typed handlers undercreateTappifyHandler({ extensionId, work }). Calls use
POST /tappify/work/<operation> and the existing authenticated install request, with current
credentials and typed input. See WorkOperationMap
for the full input/output contract.
Mutations (
configure, create, attach, comment, set_status, disconnect) carry an
idempotencyKey and ISO 8601 occurredAt. Operations on an existing item carry linkId,
externalItemId, and threadId; create carries the thread identity inside source.
Persist operation results per install and operation before acknowledging delivery. Reuse the
original result on retry and reject a key reused for a different logical mutation. eventId is
the authenticated request’s delivery identity; do not substitute a new transport ID for the
operation’s stable idempotency key.
ReviewThreadSource contains thread/project/app IDs, nullable build version/number, screen
identity, normalized pin coordinates, the first comment, safe device context, and a Tappify deep
link. Its optional screenshot descriptor carries a short-lived URL, attachment ID, media type,
and file name. The host refreshes download URLs on retries; never use a URL as mutation identity.
Create returns a stable item, the host persists its link, and separate attach calls add the deep
link and screenshot. Retrying an attachment must preserve the item and attachment identities.
Provision callbacks and verify provider webhooks
configure supplies the exact per-install callbackUrl and signing secret. Store the callback
secret encrypted alongside provider webhook routing state. Register provider webhooks against an
opaque route belonging to that install and destination; return their IDs. Keep the provider’s
webhook-verification secret separate from the Tappify callback secret.
Verify the provider signature against the exact raw request bytes before parsing or using the
payload for routing. Follow that provider’s signing/replay rules. Parsing and reserializing JSON
before verification changes the signed bytes. The backend remains provider-neutral: signature
formats, provider APIs, normalization, and provider webhook lifecycle belong to the extension.
On callback-secret rotation the host provisions another configure generation. Replace routing
and callback state atomically, invalidate stale references as appropriate, and continue using the
new callback/secret. The old callback signature no longer authenticates against the current host
secret. Never infer a callback URL or copy a shared secret from another install.
Normalize provider signals to comment.created, status.changed, or item.deleted and call
sendWorkEvent. The helper validates
the event, serializes it once, signs those UTF-8 bytes with HMAC-SHA-256, and posts to the configured
callback with x-tappify-signature. These events use the dedicated work callback, independently
of declared alert/banner/chart webhooks and sendEvent.
Every event has externalItemId, providerEventId, dedupeKey, ISO 8601 occurredAt, and optional
providerVersion. Comments include stable externalCommentId, body, author ID/display name,
and creation time. Status events include a full { id, label, stage }. Deletion needs no content.
Keep one canonical dedupe identity per logical provider signal across retries, webhook echoes,
and callback rotation. The host deduplicates by (installId, dedupeKey) and provider comment
mapping. The production Trello adapter persists one opaque event reference for each provider action
and reuses it as the dedupe key through durable delivery and hydration. Do not derive identity from
receipt time, callback generation, or a randomly generated retry ID.
For identifier-only webhooks, send a nonempty, short-lived hydrateRef in place of normalized
comment/status content. The host resolves the owned link, then calls hydrate_event with fresh
vault credentials. Return the complete event with exactly the signed type, externalItemId,
providerEventId, dedupeKey, occurredAt, and providerVersion. Bind the reference to the install,
item, owning thread, generation, and expiry. Hydration does not authorize a different event or
require the extension to retain provider access/refresh tokens for webhook processing.
Delivery, conflicts, and recovery
The host commits Review changes and durable outbox intent together, then performs provider I/O from background jobs. Provider downtime does not roll back the original Review action. Per-install delivery state and bounded retries keep one destination’s failure from replaying successful peers. The relay makes one attempt; the durable worker handles retries.sendWorkEvent performs no retry loop. It rejects non-2xx callbacks with WEBHOOK_REJECTED,
preserving the HTTP status in TapServerError. The adapter must arrange durable redelivery for
transient/network failures, processing conflicts, rate limits, and server errors with the same
event identity. Fix permanent signature, scope, ownership, configuration, or schema rejections
before redelivery. The current intake limit is 60 events per minute per install.
Return the exact provider comment ID and status version from outbound operations so webhook
echoes carry the same identities. The host stores comment mappings and provider status metadata,
suppresses duplicate visible comments, and excludes an inbound change’s source link from outbound
fanout. Delivery is at least once; visible inbound comments are deduplicated.
Prefer monotonically increasing numeric or dotted-numeric providerVersion strings; the host can
compare these and reject older status versions. Opaque versions are not lexically ordered.
Provider occurrence time is compared with the thread’s status observation time; newer observations
win. Equal or unknown observation times that disagree produce STATUS_TIMESTAMP_CONFLICT
instead of choosing a side. Provider metadata only advances monotonically; an unrelated comment
does not silently clear a status conflict. There is no general manual-resync operation in this release.
Native work chips show the extension, provider display key, normalized state, and sync health:
healthy, pending, auth_required, configuration_required, failed, or external_deleted.
The drawer offers retry for failed linked delivery, while pending work stays queued. Retry preserves
the existing link and does not call create again. Reconnect or correct configuration for the
corresponding state. A conflict is not guaranteed to clear by pressing retry on an outbound job.
item.deleted marks the link external_deleted. The host retains its identity, ignores later
comment/status changes for that deleted link, and prevents retry/recreation of the deleted item.
Uninstall attempts disconnect while credentials are available; cleanup failure is recorded but
does not prevent revocation. Remove callback secrets, routes, and hydration references. Provider
items need not be deleted; retain idempotency history according to your lifecycle policy and expire
unreachable provider webhooks through provider-owned cleanup.
Persistence boundaries
Tappify owns durable configuration revisions/generations, Review-to-provider links, comment mappings, inbound event claims/deduplication, outbox intent, and per-install delivery/retry state. Links are unique by install/thread and install/provider item. The host draws link chips from these records. The extension owns provider API behavior, encrypted callback registrations, provider webhook routing, operation idempotency results, cursors, and hydration references. Use transactional durable storage for these in a deployed extension. Hosted extension storage can hold settings and UI state, but extension servers cannot write it as a background ledger; extension storage alone is not the synchronization source of truth.Study the production Trello adapter
The production reference lives intappify-dev/extensions, under apps/trello. Start with its
README.md, then read server/work.ts, server/webhook/handler.ts,
server/webhook/normalize.ts, and server/delivery.ts.
list_containersanddescribe_containerdiscover accessible Trello boards and active lists. Configuration records one board, distinct open and resolved lists, and whether comments sync.create,attach,comment, andset_statususe durable idempotency records. Ambiguous provider responses reconcile through stable markers and provider identities instead of repeating a visible mutation.- Trello signs the untouched request body plus the exact registered callback URL. The adapter verifies that signature before parsing, normalizes supported actions, and commits an opaque hydration reference before attempting delivery.
- Durable delivery retries the same event identity.
hydrate_eventfetches current provider content with fresh install credentials and returns the complete comment or status event. - Configure generations and revisions fence stale routes. Disconnect deletes the known Trello webhook before removing its route and synchronization records.
signedWorkEventRequest and fixtures for each event type. See Testing with the SDK
and Local server for the surrounding development workflow.
The Trello package’s focused suites cover work operations, webhook signatures and normalization,
durable delivery, hydration, retries, and disconnect behavior. Its README records the redacted
staging acceptance evidence and remaining provider limitations.