refactor: lib/ is adapters only, domain lives in features/ - #366
Merged
Merged
Conversation
Moves cors/format/log/nav/request into src/shared, and reshapes lib into GitHub, revalidate, and registries adapters. Prepares the adapter floor before features/ lands in later tasks.
session.ts splits into model/queries/store/service; the node:crypto and cookie primitives stay in session.ts so model.ts remains importable from client components and the edge proxy. nav.ts moves to components/ because it carries lucide icons and a Permission type.
lib/email/client.ts keeps only the Resend calls: the outbound_messages insert moves to features/mail/store.ts, and service.ts calls the adapter and then the store, in that order.
The auth front door no longer reaches the UI barrel, so a webhook handler that wants refuse() stops pulling the console's client components. Names follow: session.ts is crypto.ts, and mail's test files say model.
maxFileBytes() reads the environment, so it stays out of the client-safe model: limits.ts resolves it and checkAttachment takes the ceiling as an argument, defaulting to what the browser already resolved.
queries.ts declared every inbox type beside its six reads; the types, the URL builder and the preview rule are now model.ts, which is what the thread list and the composers import.
Entirely pure, so the feature is model.ts plus a barrel. The editor's SEO panel is a client file and reaches it through model, not the barrel.
markdown.ts and slug.ts fold into model.ts: both are pure and imported by the "use client" editor, and model is the only pure door open to it.
registry.ts was kept import-free so the form could render labels without pulling `pg` into the bundle; that is what a model.ts is, so it is one now. settings-actions.ts came in from the lib/ root, and the single store.ts split into the read and the write.
views/members/types.ts was already a model.ts in all but name — the role shape a dropdown reads, declared so no client component imports the database package — so it is one now.
The broadcast composer comes out of the subscribers view into the new feature; the subscribers page now composes it through the front door.
The console's list and the public confirm/unsubscribe pages are the same rows and the same consent, so views/subscribers and views/subscription land in one feature.
`catalogue.ts` and `hints.ts` are pure rules and fold into `model.ts`; `collect.ts` orchestrates the registry adapters and the database, which is `service.ts`.
The overview reads from four features at once, so it is a surface rather than a slice of any one of them. It owns no server files, so it has no barrel — the route reaches its page by the page's own path, and `src/views/` is gone.
The three channel names were the one place a `lib/` file named a domain; each moves to the feature that owns it. The name strings are unchanged, so running instances still hear each other.
Routes reach their page by the page's own path, and the composer moves behind the broadcasts client door, which breaks the broadcasts/subscribers cycle.
The dashboard layout's composition moves verbatim into features/overview and the error boundary's UI into src/components; both routes are now re-export shells.
The price list is the feature's model — types, constants, pure rules — so it takes the vocabulary's name and client code reaches it through `model`.
The catalogue file is the feature's model, so it joins its component under `features/inventory` and its test keeps covering it as `model.test.ts`.
Made-to-order pricing is the custom feature's model, so it lands beside the request form as `model.ts` and its test follows it.
Leaves `lib/` holding only the env boundary, the site config and the Resend adapter, which is what issue #361 asks of it.
A root `index.ts` may not carry components, so the UI gets its own door and `features/sample`, which is all client, has only that one. Putting `store.tsx` under `components/` also stops it reading as the vocabulary's server `store.ts`.
Feature internals, lib/ reaching up into features/ and shared/ reaching anywhere are now errors. The exemptions are named per file rather than per app: narrowing a rule to one app would leave a hole, a named file is itself a rule.
Biome keys off file paths; the client boundary, the feature front door and the lawful deep reach key off file contents and directory shape. Fixture tests run each rule against a tree that breaks it.
Naming queries/store/service/events left every other feature-root file a free deep import — auth/allowlist, auth/urls and attachments/limits all walked through. The group now bans everything under a feature and re-opens model, actions and components.
Relative specifiers are resolved to their @/ form, so a cross-feature ../inbox/queries no longer walks past a rule Biome cannot see. Comments are stripped and a from-clause only counts inside an open module statement, so a commented-out import and a SQL template literal stop being imports.
home, hero-fx and playground are UI and nothing else, so each becomes a components/ folder with a client-safe barrel and no front door. blog has real server reads, so posts.ts is its queries.ts behind an index.ts.
flexiq is behind its doors now, so the biome override and the client-door/front-door exemption have nothing left to spare. With the list empty the exempt() mechanism goes too, and its fixture test flips to asserting no app is spared.
The per-app AGENTS.md files are generated by next dev and stay ignored; the root one is the committed guide, so .gitignore negates it.
types.ts and utils.ts were the features' model all along; the barrels were three export * lines carrying components. contribute's URLs now take GITHUB_URL from @byteveda/utils, which is the same string site.githubUrl was built from.
Nothing checked that a model.ts is client-safe, that a barrel is curated, or that a feature stays off the app-wide component barrel — all three are in the docs and all three passed both tools when broken.
Its whole public surface is one component, so the barrel it had was eight dead exports. The model goes beside the component that reads it, the way academy's sample and flexiq's playground keep theirs.
A model could value-import its own ./queries, which imports the driver, and both tools passed. The model and barrel checks also only saw .ts, so a model.tsx was invisible to them.
The off-vocabulary list is six entries across all four apps with features, not five in admin; and three gaps survive the new rules, including the relative spelling of the @/components ban.
Biome's paths entry only sees the aliased spelling, so ../../../components walked past it from inside a feature. Same division as feature-doors: Biome owns the alias, the script resolves the relative form.
A relative reach out of shared/ walked past Biome, and the aliased bare barrels @/lib, @/features and @/components were never in its group either. The script now mirrors the last overrides entry that lacked one.
An exported "use server" function is a URL anyone can post to, and this one handed back every broadcast's subject and body without asking.
The three copies disagreed on the trailing-slash strip and on the request fallback. The shared definition keeps the strip, which the link builders in subscribers/service.ts already applied, so no link changes.
The allowlist and the PyPI source go through lib/env like everything else, and the picker now checks against the ceiling the server resolved rather than the default a bundle is stuck with.
Biome could only say it with a paths entry, which is exact-match and so missed @/components/index, and the overrides entry carrying it duplicated the feature-doors glob list. The script now catches every spelling, and the override is gone.
It still listed the relative @/components reach as an open hole two commits after the rule that closed it.
client-door only ever inspected @/features/ specifiers, so a "use client" file could value-import @byteveda/db directly and pass. Not @/lib/*: the marketing sites legitimately render @/lib/site client-side, and what that costs is documented instead.
"The shape" claimed every app for a layout only admin reached. Names the constants still sitting in the marketing sites' lib/ and the route files that are not a few lines, as direction of travel rather than description.
submitOrder is the one-free-sample rule and templates.ts builds HTML from an OrderLine; neither is an adapter, and the exemption that called them "the Resend adapter" retires with them. Academy's lib/ is env and site.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
pratyush618
approved these changes
Sep 22, 2026
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #361.
lib/now holds only adapters to things outside the process — Resend, ioredis, the GitHub REST API, package-registry HTTP,process.env. Everything else lives infeatures/<name>/, server code beside its own UI.apps/admin/src/views/is gone.What moved
adminlib/, 47 underviews/lib/, 12 features, noviews/academypricing.ts,quote.ts,inventory.ts,orders/inlib/lib/isenv.ts+site.ts; 5 features, all with barrelsflexiqmaintypes.ts/utils.tsat feature roots,export *barrelsflexiqandmainwere not in the issue's scope. They came in because the alternative was exempting two apps from rules the other three follow, and an app the rules don't describe is an app the rules don't hold.The shape
app/→features/→lib/+shared/. One file vocabulary in every feature:model.ts(client-safe types, constants, pure rules) ·queries.ts(reads) ·store.ts(writes) ·actions.ts("use server") ·service.ts(orchestration) ·events.ts(realtime channels) ·components/·index.ts.The server/client boundary is structural rather than remembered, because
@byteveda/dbdragspginto any bundle that touches it. The rootindex.tsis the server front door and carries no components; the client-safe doors aremodel,actionsandcomponents; a route reaches its page or layout by that component's own path.That last rule is load-bearing rather than tidy. A feature barrel re-exporting a page had already dragged the console's entire client-component graph into a webhook route handler, and produced a
broadcasts → subscribers → page → broadcastsimport cycle. Both are gone.Enforcement
Rules that only exist in prose last one quarter. CI's
verifyjob now runspnpm lintandpnpm check:arch:noRestrictedImportsowns the aliased spellings — cross-feature reach past a door,lib/importing a feature,shared/importing anything of ours.scripts/check-architecture.mjs(Node built-ins only, no new dependency) owns what Biome structurally cannot: the client boundary, the front-door requirement, the route whitelist,model.ts's own client-safety, curated barrels, and the relative-path spelling of every rule above.48 fixture tests, each verified able to fail.
check:arch:testruns beforecheck:archin CI, so a check that stops checking fails loudly rather than passing silently.Behaviour changes
Three, all isolated to their own commits, each with a test that was red first:
listBroadcastshad no permission check where all four siblings did — an exported"use server"function is a public endpoint. An operator withoutbroadcasts.sendnow gets[]instead of every broadcast's subject and body. The console UI is unchanged; it only renders the composer under the same grant.origin()was defined three times, twice byte-identically. One definition inlib/env.ts. Link output is unchanged — both URL builders already stripped the trailing slash.ADMIN_MAX_ATTACHMENT_BYTESraised, the browser refused files the server would have accepted, while its own hint promised the higher number. The server-resolved limit is now passed to the picker as a required prop.Everything else on the branch is a path, an import, or a file split along a seam that already existed.
Verification
pnpm build5/5 ·pnpm lint498 files, 0 errors ·pnpm check:arch319 files, 0 violations ·pnpm typecheck10/10 ·pnpm test— admin 26 files/256 tests, academy 3/32, arch fixtures 48/48.@byteveda/main's Playwright suite fails two/newsmobile-layout cases on a React #418 hydration mismatch. This is pre-existing: the same two cases fail identically at917a99d, the branch point, verified in a clean worktree.Every commit is individually green on both tools, so the branch bisects.
Worth knowing before merge
lib/. No such module exists — the Resend client is constructed inline insideorders/service.ts, which is domain orchestration by the repo's own test ("if it knows what an order is, it is not an adapter"). Both it andemail/templates.tsmoved tofeatures/orders/, leavinglib/asenv.tsandsite.ts. The issue text may want an editor's note.apps/academy/src/lib/site.tsvalue-importsPRICINGto put a price in a meta description. Nothing behaviour-preserving removes it; the real fix — site metadata taking the price rather than importing the feature — is written beside the exemption.AGENTS.mdis honest about what no tool checks, including a client file reaching@/lib/*and gettingundefinedfromprocess.env, which is the class of bug fix 3 above corrects. That one cannot be a blanket rule: 15 client components across three apps legitimately import@/lib/{site,docs,highlight,version}.Follow-ups, deliberately not in this PR
Four dead exports (
pruneExpiredSessions,getRevision,listBroadcastAttachments,postsChanged); flexiq's three blog readers carrying three different database-failure policies;academy/features/sample/components/store.tsxcolliding with the vocabulary's reservedstore. Deleting dead code and reconciling failure policies are decisions of their own, not restructure side effects.