Files
HiveGrid-Wireframes/hivegrid-cloud/docs/architecture-migration-status.md
T

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 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.