20 KiB
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 nametoolctrl-boilerplateand root scripts that delegate to theapps/webReact Router runtime. - The root
package.jsonkeeps the top-level command surface:dev,build,start, andtypecheckdelegate 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/appis authoritative for the current web application, whileapps/*andpackages/*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/v1routes 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/v1surfaces 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 throughWORKER_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 fromdocs/deployment.md,docs/worker.md, anddocs/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 inprisma/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 inapps/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.tsandapps/web/app/modules/rows/routes/Rows_Edit.server.tsalready useUrlUtils.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 asentity.slug + "/new", and existingcurrentEntityUrl-covered route contracts rather than building every row URL inline. apps/web/app/routes/app.$tenant/g.$group/index.tsxbuilds group dashboard stat paths withappPath(...)andadminPath(...); the old direct/app/${tenantId}/...and/admin/...string-concatenation wording is stale.apps/web/app/routes/app.$tenant/g.$group/all.tsxuses route-relative creation links andEntitiesApi.getNoCodeRoutes(...)for row list routing, so it is a lower-risk migration candidate than the group stat path builder.- Rows create/edit actions accept
redirectform/query parameters and pass them toredirect(...)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.tsxbuilds sidebar destinations throughUrlUtils.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
getModulePathbehavior 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/formsand/:tenant/forms/:idviaapps/web/app/routes/app.$tenant/forms.tsxandapps/web/app/routes/app.$tenant/forms.$id.tsx. - The embeddable public runtime lives under
/embed/forms/:formKeyand 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, andsettings/formsrouting 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 usesUrlUtils.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.tsxalso contains route-relative internal links such asto="new"andto={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), andapps/web/app/modules/portals/utils/PortalUtils.tsexposesgetUrl(item, path)as a public portal URL helper based onitem.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.tsxbuilds primary new-site, detail, and settings links throughappPath(...)via a localtenantSitePath(...)wrapper.- The same route also renders external site URLs from
buildSiteUrlFromFields(site.serverHost, site.serverResolution, site.siteId). Thosehref={siteUrl}links open deployed/public sites and must stay outsideUrlUtils.getModulePath(...)because they are not internal React Router tenant destinations. apps/web/app/routes/app.$tenant/sites/new.tsxredirects newly created sites and renders the back link throughappPath(...).apps/web/app/modules/sites/components/site-form.tsxis a form and external preview surface: it slugifiessiteId, posts create/update form state, and previews deployment URLs throughbuildSiteUrlFromServer(...). 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
- Rows/Groups contract candidate: keep
apps/web/app/routes/app.$tenant/g.$group/index.tsxonappPath(...)/adminPath(...)and extend tests when new group dashboard destinations are introduced. - 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.
- Sites contract candidate: keep internal Sites links/redirects on
appPath(...); keep public and deployed site preview URLs outside internal tenant navigation helpers. - Workflow Engine contract candidate: keep
apps/web/app/modules/workflowEngine/routes/workflow-engine.view.tsxon existingUrlUtils.getModulePath(params, "workflow-engine...")usage and extend only contract tests when new Workflow Engine sections are introduced. - Portals contract candidate: keep internal Portals shell navigation on
UrlUtils.getModulePath(...), keepPortalServer.getPortalUrl(...)andPortalUtils.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.