Adds docs/plans/developer-experience-layout-refactor-2026-08-29.md and wires it into the roadmap as dated workstream lines (B3 #17, B7 #16, B9 #11) rather than a new phase. Updates the current implementation slice and README rows for B2-2 (PR #1438, B2-3/B2-4 folded in; B2-5 next) and fixes the new plan's stale pending-merge header. Claude-Session: https://claude.ai/code/session_01Rg9QQWVN3E5UUgBD2dydtu Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
28 KiB
Developer-experience and component-layout refactor plan
Status: Draft, scheduled; not started as of 2026-08-29
Measured against: 068b5ff6 (feat/b2-2-protocol-epoch)
Current sequencing: B2-2 merged to dev 2026-08-29 (PR #1438 =
9c9b8be6, B2-3 and B2-4 folded in); B2-5 is next; no work in this plan
starts before HP-2
Phase ownership: server work executes in B3 (roadmap workstream 17); client
platform and layout work executes in B7 (workstream 16), with the CSS source
split in B9 (workstream 11)
Authority: this is an implementation supplement. The
repository-health roadmap remains the
authority for phase order, entry gates, hold points, and release status.
Decision
OwnCord should refactor its internal server and client responsibilities, but it should not perform another top-level repository reorganization.
The B1 repository-foundation work already resolved the broad layout and
onboarding problems: the client is flattened under Client/, cross-component
protocol ownership lives under protocol/, active documentation is indexed,
and root commands expose bootstrap, scoped checks, generation, formatting, and
release preflight. Keep the current top-level Client/, Server/, protocol/,
deploy/, docs/, scripts/, and tools/ boundaries.
The remaining friction is inside the two application components:
- server transports, application lifecycle, persistence, and domain decisions are coupled across large packages and files;
- the client has outgrown the broad
lib/components/pages/storessplit and lacks the already-designed desktop/browser platform seam; - high-change code and its unit tests are often far apart;
- several files own multiple unrelated reasons to change.
This plan uses incremental responsibility extraction, dependency rules, and vertical slices. It explicitly rejects a wholesale rewrite, arbitrary line-count splitting, and a big-bang feature-folder migration.
Outcomes
The refactor succeeds when a new contributor can:
- find the correct server or client responsibility without tracing unrelated orchestration code;
- identify the owning tests next to, or clearly indexed from, the code;
- run a focused local check and the correct stack-level gate without guessing directories;
- add a REST endpoint, WebSocket behavior, client feature, or native capability through one documented dependency path;
- understand which files are generated and which command regenerates them;
- make changes without accidentally coupling domain behavior to HTTP, WebSocket, database, Tauri, or browser implementation details.
Non-goals
- Rename
Client/orServer/for casing or style. - Introduce a new monorepo framework or require Node for Go-only work.
- Move all server packages under
internal/for appearance alone. - Replace the existing Go service and storage layers wholesale.
- Fork desktop and browser into separate applications.
- Move all existing client unit tests in one mechanical migration.
- Change release asset names, updater contracts, protocol behavior, or generated file ownership as a side effect of layout work.
- Mix product features, security fixes, file moves, and architecture extraction in the same change.
Current-state evidence
Repository foundations to preserve
- Root
package.jsonexposesbootstrap, full and scoped checks, formatting, generation, and release preflight throughscripts/run.mjs. CONTRIBUTING.md,docs/contributing.md,docs/README.md, and the componentCLAUDE.mdfiles provide discoverable contributor entry points.Server/CLAUDE.mddocuments the current Go package responsibilities and the required default,otel,wazero, race, and deadlock variants.Client/CLAUDE.mddocuments the single WebSocket-to-store dispatch invariant, Tauri constraints, test tiers, and generated-code boundaries.protocol/schema.jsonowns generated Go and TypeScript protocol constants.- Client Vitest already includes both
tests/**/*.test.tsand colocatedsrc/**/*.test.ts, so gradual test colocation needs no new runner.
Server pressure points
Tracked production Go files currently cluster as follows:
| Package | Production files | Approximate production lines | Observation |
|---|---|---|---|
ws |
45 | 11,700 | Hub, connection, replay, broadcast, voice, persistence, and permission behavior share one package. |
db |
32 | 6,700 | Hand-written persistence and generated sqlc ownership are correctly separated, but many upper layers still import it directly. |
api |
20 | 6,400 | HTTP handlers still own persistence and domain decisions in several families. |
admin |
19 | 4,300 | Admin transport/UI handling frequently reaches directly into persistence. |
service |
18 | 4,200 | A useful domain seam exists, but it does not yet own all use cases. |
Additional evidence:
- 44 production files across
ws,admin, andapidirectly importdb: 17, 16, and 11 respectively. Server/main.goexceeds 1,000 physical lines and owns configuration, data preparation, database initialization, telemetry, plugins, event persistence, auditing, maintenance, HTTP serving, shutdown, health checks, and replay seeding.ws/hub_broadcast.go,ws/serve.go, andws/hub.goare large coordination hotspots where locking, ordering, replay, visibility, and lifecycle behavior intersect.- The existing issue register already assigns direct database ownership,
lifecycle/hub extraction, constructor completeness, and canonical permission
predicates to B3 (
S-08throughS-12).
Client pressure points
Tracked TypeScript and CSS currently cluster as follows:
| Area | Files | Approximate lines | Observation |
|---|---|---|---|
src/components |
60 | 18,400 | Shared UI, feature UI, orchestration, and native-facing behavior are mixed. |
src/lib |
63 | 15,400 | The name has become a catch-all for transport, voice, E2EE, profiles, native APIs, validation, and application orchestration. |
src/styles |
5 | 7,400 | app.css alone exceeds 5,600 physical lines and contains many feature-specific sections. |
src/pages |
19 | 5,900 | Page files and partial page subfolders show useful decomposition already in progress. |
src/stores |
9 | 2,900 | Stores are broadly imported by components, pages, and library modules. |
Additional evidence:
- Production source contains approximately 405
@lib/*, 146@stores/*, and 88@components/*import references. - 20 production frontend files import
@tauri-apps/*; the plannedsrc/platform/seam does not exist yet. - Major responsibility hotspots include
livekitSession.ts,livekitE2EE.ts,dispatcher.ts,MessageList.ts,AccountTab.ts,MainPage.ts, andapp.css. - 190 ordinary unit-test files live under
tests/unit. This makes test tiers obvious, but it often separates a module from the fastest explanation of its behavior. - Existing findings
C-11andC-12already assign import-cycle removal and cohesive hotspot extraction to B7/B9.
Architecture standards
General change discipline
- Separate pure moves, path rewrites, dependency changes, behavior changes, and generated output into reviewable commits.
- Extract by responsibility and dependency direction, not by a maximum line count.
- Introduce and test a seam before moving callers through it.
- Migrate one server domain family or one client platform capability per pull request.
- Preserve behavior with characterization, contract, race, deadlock, mutation, browser, and end-to-end tests as appropriate to the boundary.
- Add a static rule for a boundary before relying on documentation alone.
- Prefer explicit names over generic
utils,common,helpers,manager, or repository-wideinterfacespackages. - Define narrow interfaces beside their consumers. Do not build one shared interface catalog.
- Keep generated sources committed and generated from their documented source of truth; never hand-edit generated consumers.
Server dependency direction
Target dependency flow:
main / internal/app
|
v
api / admin / ws transports
|
v
service use cases and consumer-owned interfaces
|
v
db and other infrastructure adapters
Rules:
api,admin, and WebSocket message handlers translate transport input and output; they do not decide reusable domain policy.- Services own application use cases, authorization orchestration, transaction intent, and stable domain error mapping.
- Persistence packages implement narrow service-owned needs. Direct database access above services must be classified as an explicit transaction, composition, migration, or transport-adapter exception.
main.goremains the compatiblego build .entry point but becomes a thin composition root.internal/app/may own construction, start, drain, stop, maintenance, and composite cleanup because those responsibilities are application-private.- Keep
wsas one package while its locking invariants require shared private state. First split responsibilities into clearly named files; create subpackages only when doing so does not force exports or introduce cycles.
Client dependency direction
Target dependency flow:
app -> features -> shared
| |
+---------+----> platform/contracts
^
|
platform/desktop or browser
Rules:
app/owns bootstrap, composition, navigation, and the single point that registers domain dispatcher modules.features/owns cohesive user capabilities such as connection, messaging, channels, direct messages, voice, and settings.shared/contains pure reusable UI, DOM, validation, formatting, and token code. It cannot import feature state or platform implementations.platform/contracts/contains asynchronous, host-neutral interfaces only.platform/desktop/is the only ordinary production location allowed to import Tauri APIs.platform/browser/implements browser behavior or returns an explicit unsupported result. It must not imitate certificate pinning, OS keychain protection, or global push-to-talk.- Features may expose narrow public entry points. They may not import another feature's internal modules.
lib/andstores/are transitional boundaries. Shrink them as ownership is proven; do not bulk-move them merely to make the target tree appear complete.
Directional target layout
This tree describes responsibility ownership. It is not a single move list.
Server/
main.go # thin executable and composition entry
internal/
app/ # construction, lifecycle, maintenance, cleanup
api/ # HTTP transport adapters
admin/ # admin transport and server-owned admin UI
ws/ # WebSocket transport and connection mechanics
service/ # domain use cases and narrow interfaces
db/ # persistence adapters
dbgen/ # generated sqlc output
queries/ # sqlc source queries
cmd/ # standalone operator/developer tools
Client/src/
app/ # bootstrap, composition, routing, dispatch root
platform/
contracts/ # host-neutral asynchronous interfaces
desktop/ # Tauri implementations
browser/ # browser implementations or explicit refusals
features/
connection/
messaging/
channels/
direct-messages/
voice/
settings/
shared/
ui/
dom/
validation/
styles/
lib/ # transitional; retain generated protocol path
stores/ # transitional global state
Execution plan
Phase 0 — Complete B2 and freeze the contracts
Timing: now; this is prerequisite work, not layout work.
- Merge B2-2 to
devas its own completed protocol-epoch slice. - Continue the serialized B2-3 and B2-4 work and finish the remaining B2 workstreams according to the active B2 plan.
- Accept HP-2 only after protocol, trust, permission, E2EE, updater, and
compatibility evidence is green on one exact
devSHA. - Do not mix any B3/B7 structural move into a B2 change.
Exit gate: HP-2 accepted; protocol fixtures and compatibility behavior are stable enough to guard later extraction.
Phase 1 — Record boundary inventories and automated rules
Timing: B3 entry preparation after HP-2.
- Inventory every production database import in
api,admin,auth,ws, root composition, and plugin code. - Assign each call one disposition: move behind a service, retain as an explicit adapter, retain as a transaction/composition boundary, or remove.
- Inventory hub construction setters, lifecycle collaborators, lock ownership, start/stop ordering, and failure paths.
- Record before-state dependency graphs for the first server vertical slice.
- Refresh the client native-import, Rust-command, import-cycle, test-warning, timer/listener, bundle, coverage, and mutation baselines before B7.
- Add the narrowest mechanical dependency checks supported by the evidence.
Exit gate: every crossing in scope has an owner and disposition; the checks can detect a newly introduced violation without false positives.
Phase 2 — Execute the first server vertical slice
Timing: B3, before repeating the pattern elsewhere.
Use authentication as the first slice because S-10 already identifies raw
database ownership in auth routes as the intended pilot.
- Characterize authentication, enumeration defense, session, TOTP, sentinel error, and failure behavior before moving dependencies.
- Define a narrow auth use-case interface beside the HTTP consumer.
- Move authentication orchestration behind the service boundary.
- Keep persistence details in
db; preserve error semantics at the service boundary. - Retain thin request decoding, response encoding, and middleware adaptation in
api. - Record the before/after dependency graph and exact checks.
Hold point: HP-3 reviews the complete route-to-service-to-storage slice. Repeat the pattern only if it reduces coupling without weakening B2 contracts.
Phase 3 — Extract server lifecycle and hotspot responsibilities
Timing: B3 after HP-3 accepts the first slice.
- Keep
main.goas the executable entry but move application-private wiring intointernal/app/:- configuration and data preparation;
- construction and validated dependencies;
- background workers and maintenance;
- server start, drain, stop, and composite cleanup;
- failure and restart coordination.
- Replace required post-construction hub setters with validated constructor options. Leave setters only for genuinely replaceable runtime state.
- Split WebSocket responsibilities inside the existing package before
considering subpackages:
- handshake authentication;
- fresh-connect initialization;
- replay selection and delivery;
- registry and supersession;
- visibility and permission refresh;
- broadcast delivery and backpressure;
- voice session and moderation lifecycle.
- Replace mirrored permission and visibility decisions with canonical value-taking predicates and parity tests.
- Move one remaining domain family at a time behind services, with explicit transaction exceptions where necessary.
Exit gate: lifecycle ownership is explicit; required construction cannot be incomplete; every upper-layer database access is removed or justified; race, deadlock, replay, reconnect, failure-injection, coverage, and benchmark gates remain green.
Phase 4 — Establish the client platform seam
Timing: B7, after the complete beta server is stable through B6. Do not pull this work forward merely because the target directories are documented.
- Re-run the client mutation baseline before decomposition.
- Introduce typed asynchronous contracts for HTTP, WebSocket, credentials, settings/profiles, notifications, media, LiveKit proxies, logs/filesystem, window state, updater/process, opener, file selection, PTT, deep links, and application metadata.
- Add desktop implementations and move the 20 native import sites behind them one responsibility at a time.
- Add contract tests before creating browser implementations.
- Add a static rule that rejects Tauri imports outside approved desktop and bootstrap ownership.
- Split shared Vite configuration from target-specific behavior and add
explicit
build:desktopandbuild:webgates from one application tree. - Delete dead Rust command surfaces identified by the platform-contract map.
Hold point: HP-7 proves desktop parity before browser implementations are filled in.
Phase 5 — Decompose client features and hotspots
Timing: B7 for architecture and desktop parity; B9 for later feature UX and style rationalization.
Use the target layout for new or extracted code, not as a reason to bulk-move every existing file.
dispatcher.ts: retain one composition entry, but register messaging, presence, channel, DM, voice, and compatibility handlers from cohesive modules. Preserve the rule that WebSocket-driven store writes enter through dispatcher ownership.livekitSession.ts: separate session attempt ownership, room lifecycle, device/media control, remote tracks, and debug/statistics behavior without weakening supersession rules.livekitE2EE.ts: separate identity/key management, epoch transitions, worker lifecycle, participant state, and error reporting along existing staleness guards.- Messaging: separate virtual-list state, loading/windowing, renderers, attachments/media/embeds, reactions, and row lifecycle.
- Settings: split account profile, avatar, password, TOTP, status, deletion, notifications, appearance, accessibility, voice, logs, and advanced/native capabilities into owned feature sections.
- CSS: preserve selectors and built output while moving source sections into tokens/base, shared shell, messaging, sidebar, voice/video, settings, overlays, and responsive/accessibility files. Do not mix visual redesign into the source split.
- Remove production import cycles or document the smallest unavoidable seam with a boundary test.
- Centralize timer, listener, and teardown ownership for long-lived features.
Exit gate: client dependency rules pass; desktop behavior is unchanged; import cycles and unexplained warnings are gone or explicitly accepted; coverage, mutation, teardown, bundle, browser, and E2E evidence does not regress.
Phase 6 — Improve test proximity without losing test tiers
- Colocate unit tests with every newly created or materially extracted source
module using
src/**/*.test.ts. - Move an existing unit test only when its owning production module is already being extracted and the move remains reviewable.
- Keep cross-component contracts under
tests/contract, multi-module integration undertests/integration, real-browser tests undertests/browser, and Playwright undertests/e2e. - Document any fixture shared across tiers and give it one owner.
- Do not weaken assertions to make a mechanical move pass.
Exit gate: a developer opening a moved or new module can find its focused tests immediately; stack-level test commands and coverage still include every tier intended by CI.
Phase 7 — Finish the contributor path
- Add a concise change map to active contributor or architecture guidance:
- add or change a REST endpoint;
- add or change a WebSocket event;
- change the generated protocol;
- add a server domain operation;
- add a client feature or setting;
- add a desktop or browser capability;
- add a migration or query;
- choose the correct test tier.
- Add root shortcuts only for measured high-frequency development paths. Keep direct Go commands first-class and Node-free for server-only contributors.
- Print scoped commands and working directories, as
scripts/run.mjsalready does for verification. - Keep architectural ownership in one maintained map rather than adding a README to every directory.
- Exercise the guide with one fresh-contributor scenario and record every point where the contributor must guess.
Exit gate: a fresh Windows or Linux contributor can bootstrap the relevant stack, locate a common change path, run a focused test, and run the appropriate full stack check without undocumented knowledge.
Phase 8 — Exact-SHA structural qualification
Run the complete relevant matrix on the exact resulting SHA:
- all four Go build-tag variants;
- Go vet, race, deadlock, generated protocol, generated database, and lint;
- client typecheck, lint, format, unit, integration, contract, browser, and E2E;
- Rust format, tests, and Clippy;
- desktop and web production builds when B7 owns both;
- Docker and release-path checks required by the owning phase;
- generated and documentation drift checks;
- dependency-boundary and import-cycle checks;
- clean process termination and clean worktree verification.
Exit gate: every artifact and report points to the same commit; there is no accidental generated or build output; file moves are behavior-neutral; accepted exceptions have an owner, rationale, and review trigger.
Pull-request and commit strategy
Each migration unit should normally use this order:
- add or tighten characterization/contract tests;
- add the new boundary and its dependency rule;
- move one responsibility without changing behavior;
- mechanically rewrite imports and paths;
- remove the old path after all consumers move;
- run scoped checks, then the phase-required full matrix;
- update the boundary inventory and architecture/change map.
Pure moves and mechanical path rewrites should be adjacent commits. Functional changes discovered during extraction belong in separate pull requests unless a failing safety test proves they are inseparable.
Measures and acceptance criteria
Server
- Every production database import above persistence/service ownership is removed or recorded as an explicit allowed boundary.
- Authentication completes one reviewed vertical slice before the pattern is copied.
- Application construction, start, drain, stop, restart, and cleanup have explicit owners and failure tests.
- Required hub collaborators are validated at construction.
- Permission and visibility security properties have one production predicate per property.
- No extraction regresses race, deadlock, replay, reconnect, coverage, or benchmark evidence.
Client
- Ordinary production Tauri imports exist only under approved desktop adapter and bootstrap ownership.
- Desktop and web targets compile from the same application source.
- Platform contracts are asynchronous and make unsupported capabilities explicit.
- Production dependencies are acyclic, or the smallest approved exception has a boundary test and rationale.
- New and extracted unit-test ownership is colocated; higher test tiers remain centralized.
- Hotspot decomposition preserves dispatcher, voice supersession, E2EE staleness, teardown, and generated-protocol invariants.
- CSS source ownership is feature-oriented without visual behavior changes in the structural migration.
Contributor experience
- Root and per-stack commands remain discoverable and cross-platform.
- Server-only development remains possible without Node.
- A maintained change map answers where common additions belong and which tests prove them.
- Generated source ownership is visible from every relevant entry point.
- A fresh-contributor exercise completes without undocumented directory or command discovery.
Risks and controls
| Risk | Control |
|---|---|
| A large move hides behavior changes | Separate characterization, move, path rewrite, and behavior commits; restart structural review when a move commit changes logic. |
| Go subpackages force exports or cycles | Split files inside the owning package first; create a subpackage only after dependency direction is proven. |
| Service interfaces become generic abstractions | Define the smallest interface beside its consumer and implement one real use case before repeating it. |
| Desktop behavior changes during adapter extraction | Add contract tests first and reach HP-7 desktop parity before filling browser implementations. |
| Browser adapters weaken security claims | Return explicit unsupported states for certificate pinning, OS-keychain equivalence, and global PTT. |
| Feature folders become a cosmetic bulk migration | Use the target only for new/extracted responsibilities; leave transitional lib and stores until ownership is proven. |
| Test moves reduce coverage or confuse tiers | Move unit tests only with their production modules; keep contract/integration/browser/E2E tiers centralized. |
| CSS splitting becomes a redesign | Preserve selectors and rendered output; perform visual changes separately with visual/accessibility evidence. |
| Root tooling makes Node mandatory | Keep direct Go commands documented and supported; root npm remains an optional facade for server-only work. |
| Generated output is edited during moves | Preserve the source-of-truth workflows and run drift checks after every affected migration. |
First actionable slice
After B2-2 merges and HP-2 is accepted:
- write the B3 database-call and lifecycle inventory;
- record the before-state dependency graph;
- add characterization tests for auth enumeration and error mappings;
- execute the authentication route-to-service-to-storage vertical slice;
- review it at HP-3 before extracting any other server family.
Do not create Client/src/platform/ or begin client feature moves during this
slice. The client implementation work remains B7; the existing platform map is
the design input until its entry gate is met.