Summary
The backend writes digest_tasks rows without a user_id. The n8n orchestrator (workflow 3) later back-fills digest_tasks.user_id by matching task_id to a profiles row. The frontend Dashboard queries digest_tasks by user_id, so this out-of-band back-fill is load-bearing yet invisible from either repo. The backend should own user_id and write it directly.
Current state (verified)
src/state_supabase.py create_task / create_task_with_source_date (lines 27-40, 177-199) write task_id, status, message, result, user_info, timestamps — never user_id.
/generate-digest (src/main.py:301-310) builds user_info from the request body (name, title, goals, etc.) but receives no user identifier.
- Per
paperboy_all/CLAUDE.md: workflow 3 (3_Send_Digest_Email_v2, id GwegBjHAUPQO3380) "looks up the profile by task_id, back-fills digest_tasks.user_id from that profile" before the frontend can show the digest. n8n workflow 2 writes task_id back to profiles when it kicks off generation, which is what makes the later match possible.
Why this matters
- The link between a digest and the user who owns it is reconstructed in n8n, not asserted by the system that creates the digest. Break that n8n step and the frontend silently shows nothing — no error, just missing digests.
- It's fragile coupling across three systems (backend, n8n, Supabase) for what should be a single foreign key written at creation time.
- It blocks the backend from ever serving user-scoped queries itself.
Proposed approach
- Add an optional
user_id (uuid) to the /generate-digest request model (src/api_models.py) and to the n8n workflow 2 HTTP node so the orchestrator forwards profiles.id (it already has the profile in hand when it POSTs).
- Thread
user_id through to create_task_with_source_date and persist it on the digest_tasks row (src/state_supabase.py). Add the column via the migrations workflow if it isn't already present (see schema-migrations issue).
- Keep the n8n back-fill temporarily as a fallback for rows that arrive without
user_id, then remove it once all callers send user_id. Coordinate the cutover so digests never go dark.
- Update
paperboy_all/CLAUDE.md to reflect that the backend now owns user_id.
Files likely involved
src/api_models.py (add user_id to the generate-digest request)
src/main.py:289-361 (accept + pass through)
src/state_supabase.py:177-199 (persist user_id)
- n8n workflow
2_Generate_Digest (id ilWr3k5rSwCHRFXc) HTTP node — forward profiles.id as user_id. Use mcp__n8n-mcp__* to inspect/edit.
- n8n workflow
3_Send_Digest_Email_v2 (id GwegBjHAUPQO3380) — back-fill becomes a fallback, then is retired.
paperboy-frontend/src/integrations/supabase/types.ts if the column shape changes.
Acceptance criteria
Gotchas
- Sequencing is critical. If you remove the workflow-3 back-fill before workflow 2 starts sending
user_id, every new digest disappears from the UI. Land backend + workflow 2 first, verify, then retire the back-fill.
- This spans two repos + n8n. Treat it as coordinated changes, not one PR (per
paperboy_all/CLAUDE.md "two repos, two commits" rule).
- Confirm
digest_tasks.user_id type matches profiles.id type exactly to avoid join/filter mismatches.
Summary
The backend writes
digest_tasksrows without auser_id. The n8n orchestrator (workflow 3) later back-fillsdigest_tasks.user_idby matchingtask_idto aprofilesrow. The frontend Dashboard queriesdigest_tasksbyuser_id, so this out-of-band back-fill is load-bearing yet invisible from either repo. The backend should ownuser_idand write it directly.Current state (verified)
src/state_supabase.pycreate_task/create_task_with_source_date(lines 27-40, 177-199) writetask_id,status,message,result,user_info, timestamps — neveruser_id./generate-digest(src/main.py:301-310) buildsuser_infofrom the request body (name,title,goals, etc.) but receives no user identifier.paperboy_all/CLAUDE.md: workflow 3 (3_Send_Digest_Email_v2, idGwegBjHAUPQO3380) "looks up the profile bytask_id, back-fillsdigest_tasks.user_idfrom that profile" before the frontend can show the digest. n8n workflow 2 writestask_idback toprofileswhen it kicks off generation, which is what makes the later match possible.Why this matters
Proposed approach
user_id(uuid) to the/generate-digestrequest model (src/api_models.py) and to the n8n workflow 2 HTTP node so the orchestrator forwardsprofiles.id(it already has the profile in hand when it POSTs).user_idthrough tocreate_task_with_source_dateand persist it on thedigest_tasksrow (src/state_supabase.py). Add the column via the migrations workflow if it isn't already present (see schema-migrations issue).user_id, then remove it once all callers senduser_id. Coordinate the cutover so digests never go dark.paperboy_all/CLAUDE.mdto reflect that the backend now ownsuser_id.Files likely involved
src/api_models.py(adduser_idto the generate-digest request)src/main.py:289-361(accept + pass through)src/state_supabase.py:177-199(persistuser_id)2_Generate_Digest(idilWr3k5rSwCHRFXc) HTTP node — forwardprofiles.idasuser_id. Usemcp__n8n-mcp__*to inspect/edit.3_Send_Digest_Email_v2(idGwegBjHAUPQO3380) — back-fill becomes a fallback, then is retired.paperboy-frontend/src/integrations/supabase/types.tsif the column shape changes.Acceptance criteria
digest_tasksrows created by the backend includeuser_idwhen the caller supplies it.profiles.idasuser_id.user_idfor newly generated digests without relying on the workflow-3 back-fill.Gotchas
user_id, every new digest disappears from the UI. Land backend + workflow 2 first, verify, then retire the back-fill.paperboy_all/CLAUDE.md"two repos, two commits" rule).digest_tasks.user_idtype matchesprofiles.idtype exactly to avoid join/filter mismatches.