You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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:
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.
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.
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.
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.
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.
Feature parity with interlinedlist.com — tracking epic
Scope: everything the web app at
https://interlinedlist.comdoes that the iOS clientdoes 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 routefiles / 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:
app/api/**/route.ts(verb + path + auth helper:getCurrentUserOrSyncToken= Bearer-OK,getCurrentUser= session-only/mobile-blocked)diffed mechanically against every
"/api/…"string inAPIClient*.swift.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.APIClientfunction with zero call sites outside theclient 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,refreshMessageMetadataand more.destructures. Found one silent data-loss bug (
defaultVisibility→ ignored).Prior analysis lives in
work-consolidation.md; thisepic 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/closedfilter, 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, sharetokens) · push notifications.
3 · Out of scope (do not build)
for free users, never paywalled, never linked out, no price copy anywhere.
server-side key. There are no per-user LLM keys. Gate on
aiStatus().subscriberalone.admin console, blog + blog subscriptions,
architecture-aggregates,/embed/*,/api-docs,marketing pages (
/features,/pricing,/products,/about)./api/auth/{accounts,switch,remove-account}) — session-only onthe backend; a Bearer client cannot use it.
GET /api/users/lookup— unused by the web app too.4 · Workstreams
Nine independent lanes, sized so several can run concurrently. The index below is filled in
with the issue numbers.
feat/create-from-and-github-depthbranchSettingsView,EditProfileView,UserProfileView,FeedViewmenus,Models/User.swiftMessagesInboxView,DMThreadView,Models/DirectMessage.swiftOrganizationsView,APIClient+OrganizationLinkedIn.swiftDocumentsView,APIClient+Documents*.swiftListsView(ListDetailView),ListItemFormViewInterlinedListApp.swift,ILWebURL.swiftAPIClient+AppSettings.swift5 · Corrections to
work-consolidation.mdPOST /api/messages/:id/reply-countsrefreshes 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.app/documents/layout.tsxcallsgetSharedDocuments(user.id)and rendersSharedDocumentsList("Shared with me"). It is server-rendered, which is why the endpoint diff missed it. Real gap, blocked on backend ask A11./lists/:idpermalink — backend-limited (no owner in URL)"GET /api/lists/:idis 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 inInterlinedListApp.swift.6 · Working agreement (applies to every issue in this epic)
-derivedDataPath. Two worktrees testing on one UDID kill each other's runner and canexecute the other worktree's bundle (see the hazard note in
CLAUDE.md).APIClient+<Feature>.swift, never appended to theAPIClient.swiftclass body. The HTTP seam in
APIClientTransport.swiftis deliberatelyinternalso thatper-feature extension files compile.
postCamel/putCamel/patchCamelfor the manycamelCase endpoints;
post/put/patchfor snake_case. Check the neighbouring methodbefore adding one — and verify the key names the route actually destructures (see the
defaultVisibilitybug for what happens when they don't match)..swiftfile inproject.pbxproj(xcodeprojgem) — no synced groups.DispatchQueue.main.async(use
@MainActor),#Previewin every view file,.accessibilityLabelon everynon-obvious control,
privateinternals, comments only where the why is non-obvious.authState.handleUnauthorized().-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
defaultVisibilityvsdefaultPubliclyVisible) — XS/user/:u/status/:idopens the profile instead of the post — S–M (also W7)W2 · Moderation, privacy, account settings
updateUserSettings)isPrivateAccount) — SW3 · Direct messages
GET /api/dm/conversations— MW4 · Organizations
W5 · Documents
W6 · Lists
W7 · Deep links
W8 · Papercuts
APIClientfor endpoints no screen calls — S, run lastW9 · Infrastructure (not web parity)
Backend
Suggested order
already-broken-or-already-written.
W7: Open the web canonical permalinks in-app (/user/:u/status/:id, list and document permalinks) #61's dig-state fix wants A8.