Skip to content

Troubleshooting: Stuck Spinner After PDF Upload

A customer reports that the order page keeps spinning after upload, even though processing looks like it finished (e.g. Langfuse shows successful extraction traces). Use this guide to find out which half is broken: the extraction, or the poll that is supposed to notice it finished.

  1. Webapp uploads the file → creates a pdf_documents row (status pending) → starts a Temporal workflow.
  2. temporal-worker runs the extraction activities, writes extracted_orders, and updates pdf_documents.status to completed.
  3. Webapp shows the “Extracting order details…” spinner while pdf_documents.status is pending or processing.
  4. Frontend polls GET /api/pdf-documents/{documentId}. When the response has status === 'completed' it stops polling and loads the orders.

So the spinner only stops when the webapp API returns status: 'completed'. If a customer is stuck spinning, one of these is true:

  • The workflow never set pdf_documents.status to completed.
  • The poll never gets a 200 with status: 'completed' (a 400/404 keeps the frontend retrying without surfacing an error).
  • Replication lag, if the webapp reads a replica that the worker’s write hasn’t reached yet.

Using the document_id from the customer’s URL or from a Langfuse trace:

SELECT id, status, processing_error, updated_at, clerk_organization_id, user_id
FROM pdf_documents
WHERE id = '<document_id>';
  • completed with a recent updated_at → extraction worked. The problem is the poll — go to step 2.
  • pending / processing with no rows in extracted_orders → the workflow never ran or died early. Check the Temporal UI for the workflow on this document id, and the temporal-worker logs.
  • pending / processing but rows exist in extracted_orders → the activity saved orders but the status update failed. Check temporal-worker logs for the status-update failure (often a DB timeout, or an oversized original_data payload).
  • failed → extraction failed. If the customer still sees only a spinner, the poll is also failing — go to step 2.

GET /api/pdf-documents/[id] returns 200 only if:

  • the user is authenticated (locals.user);
  • for non–support-admin users, locals.organization is set (otherwise 400);
  • the document’s clerk_organization_id matches locals.organization.id (otherwise 404);
  • the document’s user_id is null or matches the current user (otherwise 404).

The frontend treats a non-ok response as transient and keeps polling, so the spinner never resolves and no error is shown.

Typical “works for me, not for the customer” causes:

  • Organization mismatch — document created under org A, customer is in org B. → 404
  • Missing organization in session — personal account, or org not loaded. → 400
  • User mismatch — user_id is set to a different member. → 404

What to check: the route logs a warning on both branches, including documentId, userId, and organizationId. Search Cloud Logging for GET pdf-document returned 404 (or returned 400 (no organization)) with the customer’s document id, then compare against the clerk_organization_id and user_id in the SQL above.

If the webapp reads a replica while the worker writes the primary, there can be a short window where the DB says completed but the API still returns the old status. This is normally seconds, not an infinite spinner — if it persists, consider reading document status from the primary on the polling endpoint.

  • Refresh the page — if the DB already says completed, a full reload skips polling and renders the orders.
  • Re-upload — creates a new document if the old row is unrecoverable.
  • Check the org — if the cause is an org or user mismatch, confirm they’re in the correct organization.
  • Polling store: apps/webapp/src/stores/pdfOrder/order.ts
  • Webapp API: apps/webapp/src/pages/api/pdf-documents/[id].ts — auth + org + user filter; emits the 404/400 diagnostic warnings referenced above
  • Extraction activities: apps/temporal-worker/activities/