Pending backend work
Frontend that shipped ahead of a backend field or endpoint — what's dark, and what unblocks it.
docs/PENDING_BACKEND.md
A living log of frontend work that shipped ahead of a backend field or endpoint it needs to be fully real. Every row here is UI that renders correctly and does not crash — it just has nothing (or fake data) behind it yet.
This is distinct from GAPS.md (backend infra/plumbing gaps with no frontend counterpart) and FEATURES.md (the broader roadmap). A row belongs here specifically when: the frontend is done, and the only thing standing between it and working is a named backend change.
Status: 🌑 dark — renders, but shows a fallback or mock data, nothing is broken · 🚧 not built — blocked entirely, no UI exists because there is nowhere to submit to yet · ✅ resolved — backend landed, frontend confirmed working (keep the row, don't delete it — dated history is the point).
Add a row the moment you ship frontend work that depends on a backend change that isn't there yet. Flip it to ✅ in the same commit that wires the real thing through, don't delete it.
Last updated: 2026-09-08 (same day, new row — Explore panel's multi-dimension filter gap).
Rows
| Date added | Feature | UI element | Files | Status | Backend requirement |
|---|---|---|---|---|---|
| 2026-09-08 | Avatars | Photo upload during invite acceptance | (not started — nowhere to submit to yet) | 🚧 not built | A self-upload endpoint (e.g. PATCH /users/me) so a user can set their own User.image. Nothing populates this column today except OAuth sign-in. |
| 2026-09-08 | Avatars | Header avatar (top-right profile menu) | apps/web/src/components/user-menu.tsx | 🌑 dark — initials only | Session User contract has no image field. Add image: z.string().nullable() to userSchema (packages/contracts/src/index.ts) and populate it in the auth/session serializer. The DB column already exists (User.image). |
| 2026-09-08 | Avatars | Assignment rows (My Work / AssignPanel) | apps/web/src/components/assign-panel.tsx | 🌑 dark — initials only | postAssignmentSchema (packages/contracts) has no image field at all. Needs the field added and the backing query updated to select it. |
| 2026-09-08 | Avatars | Approval decision list | apps/web/src/components/approval/review-path.tsx, apps/web/src/lib/approval-review.ts | 🌑 dark — initials only, but fully wired | GET /orgs/:orgId/users (apps/api/src/modules/authz/routes.ts) doesn't select image. Add image: true to the Prisma select and image: u.image to the response. One fix, benefits this row and the next one. |
| 2026-09-08 | Avatars | Approval comments | apps/web/src/components/approval/review-comments.tsx, apps/web/src/lib/approval-review.ts | 🌑 dark — initials only, but fully wired | Same fix as the row above — same endpoint, same lookup. |
| 2026-09-08 | Project onboarding form | Product type (SaaS/D2C), required | apps/web/src/app/(app)/projects/page.tsx | 🌑 dark — collected, blocks submit, not sent | Add product_type: z.enum(["saas","d2c"]) to ProjectCreateInput/Project (packages/contracts/src/projects.ts). The exact line to add it on the frontend is commented in the submit handler. |
| 2026-09-08 | Project onboarding form | Logo URL | apps/web/src/app/(app)/projects/page.tsx | 🌑 dark — collected, not sent | Add logo_url: z.string().nullable() to the same contract. |
| 2026-09-08 | Project onboarding form | Social links (Instagram/LinkedIn/Facebook/X) | apps/web/src/app/(app)/projects/page.tsx | 🌑 dark — collected, not sent | Add social_links (suggested: Partial<Record<SocialPlatform, string>>) to the same contract. |
| 2026-09-08 | Global search | Entity results (posts, campaigns, everything searchable) | apps/web/src/lib/global-search.ts | 🌑 dark — running entirely on a hardcoded mock array | No GET /search?q=… endpoint exists. The module already documents the swap: delete MOCK_SEARCH_RESULTS, point useGlobalSearch (components/search/search-palette.tsx) at the real hook — scoring/grouping/snippets are already written against SearchResult[] and don't change. Must be org-scoped server-side — the client must not be the thing filtering by tenant. |
| 2026-09-08 | Project onboarding form | Project role picked for an emailed invite | apps/web/src/app/(app)/projects/team-picker.tsx, apps/web/src/app/(app)/projects/page.tsx | 🌑 dark — role is chosen in the UI, never sent | The team picker shows one ProjectRole dropdown (Lead/Contributor/Agency/Viewer) for both a picked org member and an emailed invite, so the person filling the form only ever reasons about "their role on this project." For a picked member this is real (POST /projects/:id/members already accepts any ProjectRole). For an invite it isn't: invitationCreateSchema (packages/contracts/src/invitations.ts) has no field for an intended project role, only an org-wide role. Add project_role: projectRoleSchema.optional() to that schema, and have whatever creates the ProjectMember row on acceptance read it instead of hardcoding "contributor". Until then the frontend sends a fixed role: "member" (org role) on the invite and drops the picked project role on the floor — the accepted person always lands as "contributor" regardless of what was chosen. |
| 2026-09-08 | Overview → Explore panel | Filter row (Campaign / Placement / Device / … chips) | apps/web/src/app/(app)/projects/[id]/analytics/_kit/explore-panel.tsx | 🌑 dark — chips render, values tick, chart never narrows | Each chip's dropdown is real (lazily fetches that one dimension's distinct values via a single-dimension usePivotReport call), and ticking values updates local state with a live count badge — but the chart request never receives them. pivotQuerySchema (packages/contracts/src/kpi-pivot.ts) only accepts a single platform string filter; there is no shape for "N dimensions, each restricted to a set of values" at once (the report itself already caps at MAX_PIVOT_DIMENSIONS = 2 breakdowns, which is a different limit — filters need to apply on TOP of whatever breakdown is chosen, not compete with it for one of the two slots). Needs: a filters param on pivotQuerySchema (e.g. dimension:value1|value2;dimension2:value3, mirroring how breakdowns/metrics already arrive as delimited strings), and buildPivotReport applying each as an AND-restriction on the underlying query before aggregating. Once that lands, wire filters from MetricExplorePanel's local state into both usePivotReport calls (current + previous-period) and drop the PREVIEW badge next to the row. |
Not on this list, and why
- Currency (project form) — fully real end to end already, was already on the contract.
- Picking an existing org member for a project (project form) — fully real, including their
chosen project role. Reuses the existing
POST /projects/:id/membersendpoint, which already accepts anyProjectRole. - Team list avatars — already fully working.
GET /orgs/:orgId/membersalready returnsimage; the frontend just wasn't using it. Resolved without ever needing a row here.