Skip to content

[Epic] Feature parity with interlinedlist.com — evaluation and tracking #44

Description

@Adron

Feature parity with interlinedlist.com — tracking epic

Scope: everything the web app at https://interlinedlist.com does that the iOS client
does not, excluding billing/subscription management (never ships on iOS — App Store
Guideline 3.1.1) and the web-only surfaces listed under Out of scope below.

Evaluated: 2026-09-06, against backend ~/Codez/interlinedlist (Next.js, 228 API route
files / 296 verb+path pairs) and iOS origin/main @ 625a19b (121 Swift sources: 60 views,
19 services, 26 models; 104+ distinct verb+path call sites).


1 · Method

Four passes, cross-checked, backend source as ground truth:

  1. Endpoint diff — every app/api/**/route.ts (verb + path + auth helper:
    getCurrentUserOrSyncToken = Bearer-OK, getCurrentUser = session-only/mobile-blocked)
    diffed mechanically against every "/api/…" string in APIClient*.swift.
  2. Surface diff — every web page (app/**/page.tsx) and feature component
    (components/{lists,documents,messages,messages-dm,organizations,settings,user,sharing, scheduled,notifications,follows,shared}) compared to the corresponding iOS view.
  3. Dead-code sweep — every APIClient function with zero call sites outside the
    client itself. This is what the endpoint diff alone misses: an endpoint that is "consumed"
    by a function no screen ever calls is a missing feature, not a covered one. It found
    muteUser, trashDM, restoreDM, addOrganizationMember, joinOrganization,
    organizations, refreshMessageMetadata and more.
  4. Payload-key check — iOS request bodies vs. the fields the backend route actually
    destructures. Found one silent data-loss bug (defaultVisibility → ignored).

Prior analysis lives in work-consolidation.md; this
epic supersedes it for open work and corrects it in three places (see §5).


2 · Where iOS already is at parity ✅

Auth (email/password + OAuth: GitHub, LinkedIn, Twitter/X, Mastodon, Bluesky) · registration,
email verification, password reset, change email, sessions, delete account · feed with
compose (multi-image, video, tags, trending-tag strip, live link previews, scheduling,
cross-posting, post-as-organization), digs, replies/threads, edit, delete, search · lists
(CRUD, nesting via parentId, schema editor, rows, GitHub-backed lists with open/closed
filter, watchers, list connections, share links + email invites, lists shared with me,
public lists on profiles) · documents (CRUD, folders, markdown editor + toolbar, offline
sync with conflict merge
, collaborators, share links + invites, templates, public reader) ·
DMs (inbox/sent/deleted folders, threads, send, image attachments, unread badge) ·
notifications (list, mark read, mark all read, delete, per-event channel preferences) ·
profiles (follow/unfollow, follow requests, followers/following, block, report, CSV exports:
messages/lists/list-data-rows/follows) · organizations (create, edit, delete, members, role
changes, post as org) · sharing (unified sharing screen for lists + documents) · AI writing
assistance
(composer assistant, message series, article series, powered templates, powered
document — shipped in #43) · deep links (profile, /message/:id, /documents/:id, share
tokens) · push notifications.

3 · Out of scope (do not build)

  • Billing / subscription / Stripe — Guideline 3.1.1. Subscriber-only features are hidden
    for free users, never paywalled, never linked out, no price copy anywhere.
  • AI key entry / provider picker — AI is a subscriber entitlement on the app's own
    server-side key. There are no per-user LLM keys. Gate on aiStatus().subscriber alone.
  • Dashboard + front-wall widget layouts, engagement stats, weather/transit/markets widgets,
    admin console, blog + blog subscriptions, architecture-aggregates, /embed/*, /api-docs,
    marketing pages (/features, /pricing, /products, /about).
  • Multi-account switching (/api/auth/{accounts,switch,remove-account}) — session-only on
    the backend; a Bearer client cannot use it.
  • GET /api/users/lookup — unused by the web app too.
  • Realtime: there is no SSE/WebSocket anywhere in the product. Polling is the pattern.

4 · Workstreams

Nine independent lanes, sized so several can run concurrently. The index below is filled in
with the issue numbers.

Lane Theme Primary files (ownership boundary)
W1 Ship work already written feat/create-from-and-github-depth branch
W2 Moderation, privacy, account settings SettingsView, EditProfileView, UserProfileView, FeedView menus, Models/User.swift
W3 Direct messages MessagesInboxView, DMThreadView, Models/DirectMessage.swift
W4 Organizations OrganizationsView, APIClient+OrganizationLinkedIn.swift
W5 Documents DocumentsView, APIClient+Documents*.swift
W6 Lists ListsView (ListDetailView), ListItemFormView
W7 Deep links & permalinks InterlinedListApp.swift, ILWebURL.swift
W8 Papercuts scattered — small, sequence them under one owner
W9 Cross-device settings sync new APIClient+AppSettings.swift

5 · Corrections to work-consolidation.md

It says Correction
P6 — "batch reply counts … perf win for the feed" Wrong. POST /api/messages/:id/reply-counts refreshes cross-post reply counts from Bluesky/Mastodon/LinkedIn/X for one message on its detail view (lib/crosspost/reply-counts.ts, 10-min cache). It is a feature gap, not a perf item.
"Documents shared with me — the web lacks it too. Parity holds." Wrong. The web ships it: app/documents/layout.tsx calls getSharedDocuments(user.id) and renders SharedDocumentsList ("Shared with me"). It is server-rendered, which is why the endpoint diff missed it. Real gap, blocked on backend ask A11.
"bare /lists/:id permalink — backend-limited (no owner in URL)" Stale. GET /api/lists/:id is Bearer-ready and authorizes by role (owner/manager/collaborator/watcher), so no owner username is needed. The same stale claim is repeated in a comment in InterlinedListApp.swift.

6 · Working agreement (applies to every issue in this epic)

  • Work in your own git worktree, with your own simulator UDID and your own
    -derivedDataPath
    . Two worktrees testing on one UDID kill each other's runner and can
    execute the other worktree's bundle (see the hazard note in CLAUDE.md).
  • New endpoints go in APIClient+<Feature>.swift, never appended to the APIClient.swift
    class body. The HTTP seam in APIClientTransport.swift is deliberately internal so that
    per-feature extension files compile.
  • Pick the encoder per endpoint. postCamel/putCamel/patchCamel for the many
    camelCase endpoints; post/put/patch for snake_case. Check the neighbouring method
    before adding one — and verify the key names the route actually destructures (see the
    defaultVisibility bug for what happens when they don't match).
  • Register every new .swift file in project.pbxproj (xcodeproj gem) — no synced groups.
  • SOLID/KISS house rules: no force-unwrap in production paths, no DispatchQueue.main.async
    (use @MainActor), #Preview in every view file, .accessibilityLabel on every
    non-obvious control, private internals, comments only where the why is non-obvious.
  • A feature-endpoint 401 is not a logout — call authState.handleUnauthorized().
  • Verification is mandatory: build + full unit suite green (pinned UDID,
    -parallel-testing-enabled NO, -skip-testing:InterlinedListTests/E2EReadOnlyTests)
    before a PR is marked ready.

7 · Index

W1 · Ship work already written

Tier 0 · Broken shipped behaviour

W2 · Moderation, privacy, account settings

W3 · Direct messages

W4 · Organizations

W5 · Documents

W6 · Lists

W7 · Deep links

W8 · Papercuts

W9 · Infrastructure (not web parity)

Backend

Suggested order

  1. W1: Land Thread C — "Create from…" (G16) and GitHub label/assignee pickers (G18) #45 (land what's built) and Bug: "Default to public" never persists — iOS sends defaultVisibility, the API reads defaultPubliclyVisible #46 (data-loss bug) in parallel — both are small and both are
    already-broken-or-already-written.
  2. Backend asks blocking iOS parity — A8, A9, A11, A12 (+ A10 docs) #66 filed against the backend immediately, because W5: "Shared with me" documents (needs a backend route — A11) #54 cannot start without A11 and
    W7: Open the web canonical permalinks in-app (/user/:u/status/:id, list and document permalinks) #61's dig-state fix wants A8.
  3. Then the lanes in parallel: W2 (W2: Mute a user from the profile and post menus #47→W2: Private-account toggle in Settings (isPrivateAccount) #48→W2: View preferences — feed scope, page size, link previews, notification tray limit #49, one owner, shared files), W3, W4, W5, W6, W7.
  4. W8: Sweep APIClient for endpoints no screen calls #64 last — the sweep is only meaningful once the wiring issues have landed.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    epicTracking issue spanning several issuesparityWeb feature-parity work

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions