PDF annotations are rejected before they leave the browser, so no highlight has ever been saved in production.
Impact
A user hit this live on 2026-09-22: nine annotation.add / annotation.update rejections between 22:21 and 22:43 UTC on one checklist, each one surfacing a "Change rejected" toast. Every active production project exports zero annotations rows (checked all four: 1660, 140, 1455 and 460 answers respectively, no annotations anywhere), so this has been broken since the sync-engine migration in ba6f6c8c and simply went unnoticed until someone tried to highlight.
Cause
packages/web/src/components/pdf/embedpdf/react/src/viewer.tsx:220 spreads the raw EmbedPDF annotation object after the explicit fields:
onAnnotationAdd({
id: annotationId,
type: annotation.type,
pageIndex: event.pageIndex,
...annotation,
})
annotation.type is EmbedPDF's PdfAnnotationSubtype, a numeric enum (HIGHLIGHT = 9), so type arrives as a number. The mutator declares type: z.string() (packages/shared/src/sync/mutators.ts:1087), and the annotations row schema does the same (packages/shared/src/sync/schema.ts:127). The sync client validates args before sending, so the mutation fails fast with InvalidArgs and never reaches the Durable Object. annotation.update fails identically through updates.type (mutators.ts:1152).
The update path has the same spread at viewer.tsx:234.
Decision needed
Two ways to close it, and the choice affects the synced schema:
- Coerce at the call site - map the subtype to its name (
PdfAnnotationSubtypeName) or String(...) before handing it to the mutator. No schema change, but the stored value's meaning changes depending on which we pick.
- Widen
annotations.type and both mutators to accept the numeric subtype. Truer to the source data, but it is a schema change on a synced table.
Either way the ...annotation spread should stop clobbering the fields written above it.
Verification
- Reproduce: open any checklist with a PDF, draw a highlight, expect a "Change rejected" toast and a
client.sync.mutation_rejected line with code: InvalidArgs.
- Fixed when a highlight survives a reload and the project's sync-admin export contains
annotations rows.
PDF annotations are rejected before they leave the browser, so no highlight has ever been saved in production.
Impact
A user hit this live on 2026-09-22: nine
annotation.add/annotation.updaterejections between 22:21 and 22:43 UTC on one checklist, each one surfacing a "Change rejected" toast. Every active production project exports zeroannotationsrows (checked all four: 1660, 140, 1455 and 460answersrespectively, no annotations anywhere), so this has been broken since the sync-engine migration inba6f6c8cand simply went unnoticed until someone tried to highlight.Cause
packages/web/src/components/pdf/embedpdf/react/src/viewer.tsx:220spreads the raw EmbedPDF annotation object after the explicit fields:annotation.typeis EmbedPDF'sPdfAnnotationSubtype, a numeric enum (HIGHLIGHT = 9), sotypearrives as a number. The mutator declarestype: z.string()(packages/shared/src/sync/mutators.ts:1087), and theannotationsrow schema does the same (packages/shared/src/sync/schema.ts:127). The sync client validates args before sending, so the mutation fails fast withInvalidArgsand never reaches the Durable Object.annotation.updatefails identically throughupdates.type(mutators.ts:1152).The update path has the same spread at
viewer.tsx:234.Decision needed
Two ways to close it, and the choice affects the synced schema:
PdfAnnotationSubtypeName) orString(...)before handing it to the mutator. No schema change, but the stored value's meaning changes depending on which we pick.annotations.typeand both mutators to accept the numeric subtype. Truer to the source data, but it is a schema change on a synced table.Either way the
...annotationspread should stop clobbering the fields written above it.Verification
client.sync.mutation_rejectedline withcode: InvalidArgs.annotationsrows.