Audit — first-party manifest relationships

expand-first-party-parent-child-relationsProject noteAudits & reviews
Created openspec/changes/expand-first-party-parent-child-relations/design-notes/audit.mdView on GitHub →

Status: decided Date: 2026-04-24

Method

For every manifest under packages/polyfill-connectors/manifests/, scanned each stream for relationships[] entries and query.expand[] entries. For each relationship, checked whether the declared foreign_key exists as a top-level property on the related stream's schema (the engine join requirement — parent record key joins against child[foreign_key]).

Already shipped (kept intact)

  • gmail.messages -> message_bodies (has_one, fk on child messages_bodies.message_id)
  • gmail.messages -> attachments (has_many, fk on child attachments.message_id, default_limit=10, max_limit=50)

Newly enabled in this change

These are the only first-party relations that satisfy the engine's parent-to- child shape (parent stream declares the relationship; FK is a top-level property on the child stream):

  • slack.messages -> message_attachments (has_many, fk=message_id on child)
  • slack.messages -> reactions (has_many, fk=message_id on child)

Both child streams already require message_id and emit it as a top-level string. Both ride the same messages.sent_at timeline so child grant projection and consent_time filtering remain meaningful.

Note: the parent-side relationships[] entries are added in this change. The existing message_attachments.message -> messages and reactions.message -> messages declarations on the child streams are left in place as descriptive metadata; they are not enabled through query.expand because they are reverse (belongs-to) relations.

Deferred — reverse / belongs-to (FK on the current record, not on the related stream)

Every other declared relationship across first-party manifests is reverse. The current engine cannot serve them, and this change does not add a reverse-lookup contract.

ConnectorStreamRelationshipReason deferred
amazonorder_itemsorder -> ordersfk order_id lives on order_items, not on orders
anthropicmessagesconversation -> conversationsfk on parent record
chasetransactions, statements, balancesaccount -> accountsbelongs-to
chatgptmessages, shared_conversationsconversation -> conversationsbelongs-to
claude_codemessages, attachmentssession -> sessionsbelongs-to
codexmessages, function_callssession -> sessionsbelongs-to
doordashorder_itemsorder -> ordersbelongs-to
heborder_itemsorder -> ordersbelongs-to
loomtranscriptsvideo -> videosbelongs-to
slackchannel_membershipschannel/userbelongs-to
slackmessageschannel/authorbelongs-to (channel/user lookup)
slackmessage_attachmentsmessagebelongs-to
slackreactionsmessage/userbelongs-to
slackcanvaseschannel/authorbelongs-to
slackdm_read_stateschannelbelongs-to
usaatransactions, statements, credit_card_billingaccount -> accountsbelongs-to
whatsappmessageschat -> chatsbelongs-to
ynabaccounts/categories/payee_locations/transactions/scheduled_transactions/month_categoriesbudget/payee/category/account/group/etc.belongs-to

Rejected candidates

  • slack.messages -> files: files is keyed by file id, but the message↔file edge does not exist as a top-level FK on files. Slack files are referenced through message.has_files and a separate (not currently modeled) join table. The engine cannot serve this until either an explicit edge stream is added or a reverse-lookup contract exists.
  • slack.channels -> messages, slack.channels -> channel_memberships, slack.channels -> canvases, slack.channels -> dm_read_states: technically satisfy the parent-to-child shape (channel id is a top-level FK on each child), but were rejected for this tranche because:
    • channels -> messages is the most-loaded fan-out in the entire corpus and needs a dedicated cardinality / pagination review;
    • the others are operationally low-value compared to the engineering cost of enabling them on every channels read;
    • they can be added in a follow-up tranche once the messages -> children shape is in production and we have a usage signal.
  • slack.users -> reactions, slack.users -> channel_memberships, slack.users -> canvases (author): same argument; user-fanout is huge and the reactions/memberships streams are usually read directly, not through user expansion.
  • All YNAB *-by-budget relations: technically parent-to-child for budgets -> accounts/categories/..., but the YNAB connector is small enough that callers can read each stream directly. Holding until there is demand.