# Architecture Migration Status This document is the canonical current-state snapshot for the repository architecture migration. It describes what is present today, which boundaries are already runtime-wired, and which surfaces remain skeleton-only so later pruning and extraction work can use one repo-state contract. ## Workspace and package manager state - The repository is a private pnpm-managed project (`packageManager: pnpm@10.33.0`) with root package name `toolctrl-boilerplate` and root scripts that delegate to the `apps/web` React Router runtime. - The root `package.json` keeps the top-level command surface: `dev`, `build`, `start`, and `typecheck` delegate into the workspace-owned web runtime and shared package boundaries. - Workspace dependencies are already declared for the target package boundaries through `workspace:*` imports such as `@toolctrl/config`, `@toolctrl/db`, `@toolctrl/mail`, `@toolctrl/queue`, and `@toolctrl/ui`. - The migration is therefore no longer root-hybrid for web source ownership: `apps/web/app` is authoritative for the current web application, while `apps/*` and `packages/*` provide conservative target boundaries for later route-by-route or category-by-category extraction. ## Boundary status | Boundary | Status | Current state | | ----------------- | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `apps/web` | runtime source home | Physical home for the React Router web app under `apps/web/app` and `apps/web/public`; root-level commands still own the top-level developer entrypoints. | | `apps/api` | runtime-wired API app | Dedicated Hono API app owns the canonical `/v1` route surface, route-access inventory, OpenAPI docs, health/readiness routes, and package scripts for dev/build/start/typecheck. Some `/v1` routes still proxy web-owned internal handlers until their service code is extracted, so document both the canonical API route and any temporary web implementation owner. | | `apps/worker` | runtime-wired foundation, disabled by default | BullMQ worker entrypoint exists and can be run directly, but exits unless `WORKER_ENABLED=true`. It owns concrete handler paths for mail, webhooks, notifications, maintenance, storage scans, exports, imports, guarded AI/browser descriptors, `worker-general` outbox dispatch, `outbox.workflow`, `outbox.plugin-hook` fanout, and isolated `worker-plugin` invocation. Live Coolify deployment proof still belongs to the deployment/workers runbooks, not this architecture snapshot. | | `packages/config` | partially runtime-wired catalog | Typed environment catalog and validation helpers document the current environment surface and are wired into web/API/worker runtime loaders plus env drift checks. | | `packages/db` | partially runtime-wired boundary | Owns the server-side Prisma client boundary through `@toolctrl/db/client.server`, the Prisma 7 generated TypeScript client boundary at `packages/db/generated/prisma/client`, and the package command surface for generate, migrate deploy and seed. The schema and migrations still physically live under `prisma/` and are reached through the root Prisma config. | | `packages/mail` | runtime-wired source boundary | Provides the Mailjet provider adapter and `MailProvider` contract for transactional mail. It is source-only but represents the active provider boundary that later API/worker runtime code should instantiate. | | `packages/queue` | runtime-wired queue contract | Defines BullMQ queue names, typed payloads, producer helpers and factory helpers consumed by web/API enqueue paths and `apps/worker` processors. | | `packages/shared` | skeleton-only | Intentionally empty cross-cutting package reserved for dependency-free shared types, utilities and constants once ownership is clear. | | `packages/ui` | skeleton-only adapter contracts | Source-only UI adapter boundary. It exposes stable adapter metadata for selected categories but does not render components, import React or replace current app imports yet. | ## Runtime-wired versus skeleton-only summary Runtime-wired or partially runtime-wired surfaces: - `apps/web`: physical React Router web app source and public asset home; root scripts delegate into this runtime. - `apps/api`: independent Hono API app and server scripts are present; current `/v1` routes include health/docs, auth/session, API-key validation, accounts/usage, analytics, notifications, workspaces, entities, relationships, exports, billing webhook ingress, and maintenance enqueue behavior. Widget and workflow `/v1` surfaces are plugin-disabled historical code paths, not active canonical API routes. Temporary proxy routes must be documented as API-owned entrypoints with web-owned implementation handlers until extracted. - `apps/worker`: executable worker boundary exists but is intentionally opt-in through `WORKER_ENABLED=true`; processors are wired for the current queue payload contracts, including the five configured worker groups (`general`, `mail`, `import`, `webhook`, `plugin`) and the worker-general outbox/Workflow/Plugin-Hook fanout paths. This is repository/runtime contract evidence; environment-specific Coolify health still comes from `docs/deployment.md`, `docs/worker.md`, and `docs/ops/worker-queues.md`. - `packages/db`: server-side Prisma client boundary, Prisma 7 generated TypeScript client output, and database command entrypoints exist in the package boundary; the schema and migrations still live in `prisma/` and are referenced by root Prisma config. - `packages/mail`: Mailjet provider contract and adapter exist as the transactional mail boundary. Skeleton-only or documentation/contract surfaces: - `packages/shared`: intentionally empty until clear shared ownership emerges. - `packages/ui`: adapter metadata and migration contracts only; current UI runtime imports remain in `apps/web/app/`. ## Architecture Closure final closure status Architecture Closure closes the architecture snapshot as a handoff contract, not as a claim that every feature has already been deleted or extracted at runtime. API, worker and package boundaries are documented above: `apps/api`, `apps/worker`, `packages/config`, `packages/db`, `packages/mail` and `packages/queue` have concrete or partial runtime wiring, while `apps/web` owns the web source tree and `packages/shared` and `packages/ui` remain skeleton-only or contract surfaces until sequential atomic migration tasks move ownership. This preserves R001 handoff coverage and R002 sequencing by keeping the current React Router runtime authoritative until each downstream extraction replaces it directly. The feature-pruning contract is now anchored in `docs/feature-manifests/` and validated by `apps/web/app/application/featureManifests.test.ts`. `docs/feature-inventory.md` remains the human-readable Core/Plugin/Removal inventory, while the manifests are the machine-readable source for `core`, `plugin-candidate`, `remove-candidate` and related pruning actions. Under R003, these classifications are removal and replacement instructions for future work; they are not a permanent legacy-support promise and should not be interpreted as runtime plugin extraction already completed. S04 bundle/performance evidence is represented by the static build-boundary helpers `apps/web/app/utils/build/bundleScan.ts` and `apps/web/app/utils/build/manualChunks.ts`, which make heavy feature/dependency surfaces visible before pruning work removes or packages them. The optional manual follow-up remains `pnpm build`: use it as a human verification step for production bundle behavior when needed, but do not treat its absence here as contradicting the documented Architecture Closure closure. ### Portals/Sites DB Ownership D006 records the DB ownership exception for Portals/Sites: do not move these persistence calls into `packages/db` as part of the current DB/Prisma ownership work. Until the feature/plugin decision determines whether Portals/Sites are extracted as optional plugins or removed from the default boilerplate, `apps/web/app/modules/portals/db/*` and `apps/web/app/modules/sites/db/sites.db.server.ts` remain the accepted temporary module-owned persistence boundaries. This is a documented deferral, not full R006 validation for Portals/Sites, and it is separate from the URL-boundary guidance below for internal tenant navigation versus public portal/site URLs. ## R003: clean replacement and removal rule R003 is implemented as a pruning and boundary rule, not as a compatibility promise. Replaced or removed features should be either cleanly implemented in their target boundary or deleted from the active runtime; they should not be preserved through permanent legacy adapters, shadow call sites, or indefinite compatibility layers. Temporary coexistence is acceptable only while a later migration task explicitly owns extraction and verification. Once a feature is marked as replaced or removed in the inventory/manifests, future implementation work should prefer direct deletion, direct replacement, or a short-lived migration step with clear removal criteria. ## UrlUtils migration inventory This inventory tracks feature-specific URL construction that should move toward explicit `UrlUtils` helpers instead of legacy string concatenation or broad adapters. Each item records the current state, the first clean migration unit, and the scoped verification expected for that unit. ### Rows/Groups Status: mixed `UrlUtils` adoption with concrete hard-path follow-ups. - `apps/web/app/modules/rows/routes/Rows_New.server.ts` and `apps/web/app/modules/rows/routes/Rows_Edit.server.ts` already use `UrlUtils.currentTenantUrl(params, "404")` for tenant-scoped 404 redirects when autogenerated/system entity checks fail. - Row create/edit flows delegate row destinations through `EntitiesApi.getNoCodeRoutes(...)`, `EntityHelper.getRoutes(...)`, route-relative links such as `entity.slug + "/new"`, and existing `currentEntityUrl`-covered route contracts rather than building every row URL inline. - `apps/web/app/routes/app.$tenant/g.$group/index.tsx` builds group dashboard stat paths with `appPath(...)` and `adminPath(...)`; the old direct `/app/${tenantId}/...` and `/admin/...` string-concatenation wording is stale. - `apps/web/app/routes/app.$tenant/g.$group/all.tsx` uses route-relative creation links and `EntitiesApi.getNoCodeRoutes(...)` for row list routing, so it is a lower-risk migration candidate than the group stat path builder. - Rows create/edit actions accept `redirect` form/query parameters and pass them to `redirect(...)` when present. That trust boundary is intentionally only inventoried here; a later security-focused task should normalize same-origin/app-local redirects before changing behavior. First atomic migration plan: keep `apps/web/app/routes/app.$tenant/g.$group/index.tsx` on `appPath(...)`/`adminPath(...)`, keep route-relative links in `all.tsx` unchanged, and extend the focused URL tests when new group dashboard destinations are introduced. ### Workflow Engine Status: already aligned with direct tenant module URL rules. - `apps/web/app/modules/workflowEngine/routes/workflow-engine.view.tsx` builds sidebar destinations through `UrlUtils.getModulePath(params, "workflow-engine...")`, matching the current direct tenant module rule for Workflow Engine sections such as workflows, variables, credentials, executions, templates, and danger. - The shell route files for Workflow Engine remain thin delegates into `apps/web/app/modules/workflowEngine/routes/**`; the module route file is the authoritative URL construction surface for the sidebar. - No legacy adapter is needed for Workflow Engine under R003 because the current call sites already target the canonical `getModulePath` behavior instead of preserving parallel hard-coded `/app/${tenant}` module strings. First atomic migration plan: keep `workflow-engine.view.tsx` on `UrlUtils.getModulePath(...)` and add regression coverage only when new Workflow Engine shell routes or sidebar entries are introduced. Candidate paths are `apps/web/app/modules/workflowEngine/routes/workflow-engine.view.tsx` and the shell route delegates that import from `apps/web/app/modules/workflowEngine/routes/**`; verification is the focused UrlUtils contract asserting `workflow-engine` and nested `workflow-engine/*` paths for tenant params. ### Forms/FormBuilder Status: active as Form Widget runtime; legacy form/survey routes remain removed. - Tenant form administration lives under the direct tenant routes `/:tenant/forms` and `/:tenant/forms/:id` via `apps/web/app/routes/app.$tenant/forms.tsx` and `apps/web/app/routes/app.$tenant/forms.$id.tsx`. - The embeddable public runtime lives under `/embed/forms/:formKey` and the submit API uses `/api/embed/forms/:formKey/submit`; these are public/embed boundaries, not tenant module navigation paths. - Removed legacy surfaces stay removed: public `/f`, `/surveys`, form-based Help Desk survey routes, and `settings/forms` routing must not be reintroduced by default. ### Portals Status: mostly aligned with `UrlUtils.getModulePath(...)` for internal tenant navigation, with a separate public portal URL boundary. - The authenticated tenant shell under `apps/web/app/routes/app.$tenant/__portals/**` is the internal navigation surface for Portals. The list route uses `UrlUtils.getModulePath(params, "portals/new")` for new-portal navigation, and the portal detail shell builds paths from the canonical portal subdomain (`portal.subdomain || params.portal`) for overview, pages, users, analytics, pricing, domains, settings, and danger sections. - `apps/web/app/routes/app.$tenant/__portals/portals.tsx` also contains route-relative internal links such as `to="new"` and `to={item.subdomain}`. These remain React Router tenant-shell links today; they are inventory candidates for explicit contract coverage rather than immediate migration because the surrounding route already establishes the tenant module context. - External portal URLs are built outside the tenant-app route contract. The list loader obtains public URLs through `PortalServer.getPortalUrl(item)`, and `apps/web/app/modules/portals/utils/PortalUtils.ts` exposes `getUrl(item, path)` as a public portal URL helper based on `item.url`. - Public portal URLs and portal server/domain links are external navigation boundaries. They must not be normalized through `UrlUtils.getModulePath(...)`, which is reserved for internal tenant React Router destinations. - Under R003, Portals do not need a legacy URL adapter: current internal shell navigation already uses canonical module-path helpers, while public portal URLs should keep a separate public URL contract. First atomic migration plan: add Portals contract assertions to `apps/web/app/utils/app/UrlUtils.test.ts` for tenant module paths such as `portals`, `portals/new`, `portals/:portal/pages`, and `portals/:portal/settings`; keep `PortalServer.getPortalUrl(...)` and `PortalUtils.getUrl(...)` outside `UrlUtils.getModulePath(...)`; then consider replacing route-relative list/detail links only if a later task introduces a concrete helper for portal subdomain detail paths. ### Sites Status: helper-built internal tenant paths with a separate external site preview/deployment URL surface. - `apps/web/app/routes/app.$tenant/sites/$type.tsx` builds primary new-site, detail, and settings links through `appPath(...)` via a local `tenantSitePath(...)` wrapper. - The same route also renders external site URLs from `buildSiteUrlFromFields(site.serverHost, site.serverResolution, site.siteId)`. Those `href={siteUrl}` links open deployed/public sites and must stay outside `UrlUtils.getModulePath(...)` because they are not internal React Router tenant destinations. - `apps/web/app/routes/app.$tenant/sites/new.tsx` redirects newly created sites and renders the back link through `appPath(...)`. - `apps/web/app/modules/sites/components/site-form.tsx` is a form and external preview surface: it slugifies `siteId`, posts create/update form state, and previews deployment URLs through `buildSiteUrlFromServer(...)`. This slice should not change its internal behavior or route paths; later URL work should treat tenant/site IDs as normalized inputs before they flow into internal UrlUtils helpers, while keeping deployment preview URLs on the Sites public URL helpers. First atomic migration plan: keep `apps/web/app/routes/app.$tenant/sites/$type.tsx` and `apps/web/app/routes/app.$tenant/sites/new.tsx` on helper-built internal destinations; keep `site-form.tsx` preview/deployment URL construction unchanged and explicitly outside internal tenant-app URL helpers. ### First UrlUtils migration backlog 1. Rows/Groups contract candidate: keep `apps/web/app/routes/app.$tenant/g.$group/index.tsx` on `appPath(...)`/`adminPath(...)` and extend tests when new group dashboard destinations are introduced. 2. Forms contract candidate: keep tenant Forms redirects and overview links on helper-built module paths while leaving public form URLs and external redirects on a separate public-form/security contract. 3. Sites contract candidate: keep internal Sites links/redirects on `appPath(...)`; keep public and deployed site preview URLs outside internal tenant navigation helpers. 4. Workflow Engine contract candidate: keep `apps/web/app/modules/workflowEngine/routes/workflow-engine.view.tsx` on existing `UrlUtils.getModulePath(params, "workflow-engine...")` usage and extend only contract tests when new Workflow Engine sections are introduced. 5. Portals contract candidate: keep internal Portals shell navigation on `UrlUtils.getModulePath(...)`, keep `PortalServer.getPortalUrl(...)` and `PortalUtils.getUrl(...)` as public portal URL boundaries, and add/maintain contract assertions for portal module paths before replacing route-relative links. ## Next implementation inputs Use these files as the concrete inputs for follow-up implementation and pruning work: - `docs/feature-inventory.md` — human-readable feature inventory and keep/plugin/remove/status classification. - `docs/feature-manifests/*.json` — machine-readable feature manifests for boundary ownership, pruning decisions and downstream task planning. This status snapshot should be updated whenever a boundary changes from skeleton-only to runtime-wired, when root runtime ownership moves into an `apps/*` or `packages/*` workspace, when feature-specific UrlUtils migration status changes, or when R003-driven pruning changes the active feature surface.