This is the main architecture guide for RouteVN Creator.
It defines:
- the major layers of the app
- the intended dependency direction
- the authoritative code-placement rules
- the current canonical patterns we want to preserve
If architecture or code-placement boundaries change, update this document in the same PR.
AGENTS.md is still the source of truth for coding-agent workflow rules and
coding conventions. This document should stay architecture-first.
Frontend:
@rettangoli/ui@rettangoli/fe@rettangoli/vt
Desktop shell:
Tauri
Local-first collaboration:
insieme
- Prefer direct values and
??defaults over verbose string guards liketypeof value === "string" && value.length > 0 ? value : nullwhen the data contract is already stable. - Use simple normalization such as
value ?? nullunless real runtime type narrowing is required.
This repo currently uses two main test styles:
- script-driven Node tests in
scripts/test-*.js - YAML-driven Puty tests in
tests/puty/
Common commands:
bun run test:smoke
bun run test:integration
bun run test:convergence
bun run test:collab-adapters
bun run test:putyRun one Puty scenario directly:
bunx vitest run tests/puty/<file>.spec.yaml-
scripts/test-smoke.jsbroad project-state and reducer smoke coverage -
scripts/test-integration.jscollaboration/session integration flows -
scripts/test-convergence.jsmulti-client convergence behavior -
scripts/test-collab-adapters.jscollab adapter coverage -
tests/puty/*.spec.yamlSQLite-backed Insieme storage scenarios expressed declaratively in YAML -
tests/puty/insiemeStorageScenario.jsshared Puty helper that runs a real RouteVN collab session against the SQLite sync store and returns committed rows for YAML assertions
Use Puty for storage assertions when the goal is:
- define a full command sequence in YAML
- submit it through the real collab/session path
- assert the committed events that land in SQLite
Keep the test contract declarative:
in: the commands to submit, or batches of commands to submitout: the normalized committed rows expected in storage
Current Puty coverage is intentionally limited to client-owned storage behavior:
- mixed command persistence across partitions
- storage idempotency across submit batches
File-type policy for uploads is documented in docs/upload-file-types.md.
If an upload surface changes its accepted file types or validation behavior, update that document in the same PR.
RouteVN is organized into four ownership zones:
- UI surfaces
src/pages/src/components/src/primitives/
- App-owned shared code
src/internal/project/src/internal/ui/- small pure helpers in
src/internal/
- Services
src/deps/services/
- Clients
src/deps/clients/
The intended dependency direction is:
pages/components/primitives
-> internal/ui
-> appService / projectService
-> deps/services
-> deps/clients
internal/ui -> internal/project
deps/services -> internal/project
Not every path needs every layer. The important rule is ownership:
- UI-facing shared glue goes in
src/internal/ui/ - pure project meaning goes in
src/internal/project/ - handler-facing facades and service adapters go in
src/deps/services/ - low-level platform/external adapters go in
src/deps/clients/
High-level rules:
- views stay declarative
- stores hold UI-local state and derived display data
- handlers orchestrate
src/internal/ui/owns shared page/store/handler orchestrationsrc/internal/project/owns project meaning and invariantssrc/internal/project/stays intentionally merged into a small number of files, not many sparse helpers- small pure app-owned helpers that are neither project semantics nor UI
orchestration stay in
src/internal/ src/deps/services/owns service behavior and service adapterssrc/deps/clients/owns low-level platform/external clients- setup entry points create dependencies
Repository-facing product concepts must be modeled directly in the repository schema and creator-model commands.
Do not solve schema gaps with UI-only reinterpretation such as:
- storing one product resource type inside another resource collection
- renaming a concept only in the UI while keeping a different underlying repository type
- adding projection/export-time translation layers to compensate for missing repository support
Projection is allowed to adapt repository data to downstream runtime contracts, but it must not be used to hide missing first-class repository concepts.
If RouteVN Creator introduces a user-visible resource such as controls, the
expected implementation path is:
- add the resource to
routevn-creator-model - add the corresponding repository commands and validation
- update client pages to use that repository resource directly
- keep projection as a runtime adapter only
If this architectural boundary changes, update this document in the same PR.
- web:
src/setup.web.js - desktop:
src/setup.tauri.js
These entry points are responsible for:
- creating client dependencies
- creating services
- exposing dependencies through
deps - registering browser primitives
Handlers must not create dependencies themselves.
Preferred flow:
route change
-> app-level route orchestration resolves path + payload
-> if the route needs a project repository, projectService ensures it
-> target page mounts
-> project-backed pages subscribe to project state
-> selectors derive view data
-> render
Page handlers should not repeat route-level repository boot logic.
Repository state is the authoritative source of truth for project-backed pages.
Preferred flow:
command or remote collab event
-> repository state changes
-> projectService emits subscribed state update
-> page store updates subscribed snapshot
-> selectors derive view data
-> render updates
Avoid this older pattern:
mutation
-> page calls refresh handler
-> page copies repository slices manually
-> render
projectService.subscribeProjectState(...) is synchronous and assumes
app-level orchestration already ensured the repository before page mount.
Preferred flow:
collab session receives committed remote event
-> repository state updates
-> subscribed pages rerender from repository state
-> optional normalized collab events are published for page-owned policy
Important boundary:
- collab runtime must not scan the DOM for mounted pages
- collab runtime must not know page tags or handler names
- page refresh policy belongs in page/family orchestration while the repo is still migrating to repository-driven rendering
Preferred structure:
editableText primitive
-> linesEditor component
-> internal/ui scene editor helpers
-> sceneEditor page
-> projectService / internal/project
This keeps low-level caret and contenteditable behavior separate from scene-specific workflows.
-
src/pages/Route-level screens and screen-specific orchestration. -
src/components/Reusable UI building blocks. -
src/primitives/Browser-native custom elements and low-level DOM wrappers. -
src/internal/App-owned shared logic.src/internal/project/is reserved for pure project semantics and should stay intentionally small and flat:commands.jsstate.jsprojection.jstree.jslayout.jssrc/internal/ui/is the only shared home for page/store/handler orchestration that does not belong inside one page folder.
-
src/deps/services/shared/Shared handler-facing and internal service logic. -
src/deps/services/web/Web-specific service adapters. -
src/deps/services/tauri/Desktop-specific service adapters. -
src/deps/clients/Low-level platform and external adapters such as router, DB, pickers, updater, file processing, and template loading. -
src/deps/services/shared/collab/Shared collaboration/session logic. -
src/deps/services/web/collab/Web transport/runtime-specific collaboration wiring.
Detailed “when adding X, put it here” contribution rules belong in AGENTS.md.
This document should explain the shape of the architecture, not act as a
contribution checklist.
Deprecated folders:
src/deps/features/is a legacy location. Do not add new code there. Shared page/store/handler orchestration belongs insrc/internal/ui/.src/deps/infra/is a legacy location. Do not add new code there. Low-level platform/external adapters belong insrc/deps/clients/.
UI handlers should normally talk only to:
appServiceprojectService
Handler-facing facade for:
- navigation
- dialogs and toasts
- dropdowns
- user config
- project entry management
- file picking
- app/platform metadata
Handler-facing facade for:
- repository access
- command submission
- asset upload and retrieval
- collaboration sessions
- export/bundle operations
Handlers should not orchestrate multiple internal services directly when those concerns belong behind one of these facades.
Exported package.bin files have two distinct version concepts:
-
binary format version
- stored in byte
0of the bundle header - currently
2 - used by the bundled player to decide whether it can parse the bundle at all
- stored in byte
-
bundler metadata
- stored inside the JSON
instructionspayload asbundleMetadata.bundler - currently includes:
appNameappVersion
- used for provenance, debugging, and support
- stored inside the JSON
Do not collapse these into one field.
- format version answers: "Can this runtime parse the bundle structure?"
- bundler metadata answers: "Which app/version produced this artifact?"
The canonical implementation points are:
- writer:
src/deps/services/shared/projectExportService.js - bundle-page export path:
src/pages/versions/versions.handlers.js - reader:
scripts/main.js
If the bundle contract changes, update the smoke test in
scripts/test-smoke.js in the same PR.
Service boundary test:
- if code needs store setters/selectors, refs,
render(), or page event payloads, it is not service code; it belongs insrc/internal/ui/or a page - if code wraps router, DB, file picker, updater, browser storage, or similar
external/platform APIs, it is client code; it belongs in
src/deps/clients/
When deciding where new code goes, apply these rules in order:
- If it owns one route or screen, put it in
src/pages/. - If it is reusable visual UI, put it in
src/components/.src/components/<name>/is limited to Rettangoli FE component files only. Do not add ad hoc sibling helper modules there. - If it owns low-level DOM or browser-native behavior, put it in
src/primitives/. - If it is shared UI/page/store/handler glue and may touch refs, render,
stores, RxJS,
appService,projectService, or event subjects, put it insrc/internal/ui/. - If it changes project meaning, command semantics, state semantics,
projection, tree behavior, or layout semantics, put it in
src/internal/project/. - If it is a small pure app-owned helper that is not project-specific and not
UI orchestration, put it in
src/internal/. - If it is a handler-facing service or a service adapter, put it in
src/deps/services/. - If it is a low-level platform/external adapter, put it in
src/deps/clients/.
If something does not fit cleanly, do not invent a new top-level bucket. Refine one of these existing boundaries instead.
Most pages and many components use Rettangoli’s fixed file pattern:
*.view.yaml*.store.js*.handlers.js
Allowed component-folder files are Rettangoli FE files only, for example:
*.view.yaml*.store.js*.handlers.js- optional FE companion files such as
*.schema.yaml,*.constants.yaml, or*.methods.js
Do not place ad hoc helper modules, editor-specific orchestration files, or
other non-FE support files inside src/components/<name>/.
If code is shared or specific but is not itself one of the component FE files, put it somewhere else based on ownership:
src/internal/ui/for shared UI/store/handler orchestrationsrc/internal/project/for project semanticssrc/internal/for small pure app-owned helperssrc/deps/services/orsrc/deps/clients/for service/client-owned code- the owning page folder when the code is page-specific
The intended split is:
view: declarative structurestore: UI-local state and derived display datahandlers: orchestration
Handler modules must be safe for multiple mounted instances at the same time.
Do not use:
- module-scoped mutable runtime state in
*.handlers.js refs.__...Runtime- cleanup functions, callbacks, or service instances in store
Preferred options:
handleBeforeMountcleanup closures when one lifecycle owns the state- RxJS streams/subscriptions for project or collab lifecycles
- RxJS stream composition for short-lived app-level event windows such as key chords
- explicit top-level store fields only for plain local values such as timer ids or cache entries
Page and component handlers must not reach for browser globals directly for cross-cutting side effects such as:
- global focus changes
- history manipulation
- global listeners
- DOM queries outside the local owned surface
Put those behind:
src/deps/services/shared/*src/deps/clients/*src/primitives/*
Allowed exceptions:
- local DOM behavior inside a primitive
- local DOM behavior inside a component that owns that DOM editing/interaction surface
src/internal/project/ owns:
- project meaning
- command semantics
- invariants
- state projection
- tree behavior
- layout semantics
It is intentionally constrained to five canonical files:
commands.jsstate.jsprojection.jstree.jslayout.js
Prefer merging related project helpers into those files over creating sparse
new src/internal/project/* modules.
If a rule changes what a project means, it belongs in src/internal/project/, not in
stores or handlers.
Project-authored collection items should converge on the generic
resource.* family unless they are truly document-internal structures.
Use resource.* for collection-level lifecycle such as:
- create
- rename
- move/reorder
- delete
- duplicate
This applies to normal resource collections and also to authored collections such as:
variableslayoutsat the collection/item level
Keep separate command families only where the structure is not just a collection item lifecycle. Current examples:
scene.*section.*line.*character.sprite.*layout.element.*
The intended model is:
variablesbelong under the generic resource familycharactersuseresource.*for character lifecyclecharacter.sprite.*remains separate for internal character sprite tree editinglayoutsshould also useresource.*for collection lifecyclelayout.element.*remains separate for internal layout document editing
Platform-specific differences belong in:
src/deps/clients/*src/deps/services/web/*src/deps/services/tauri/*
Do not scatter platform conditionals through page handlers or domain code.
Project name, description, and iconFileId are owned by the
project-specific DB app store as projectInfo, not repository state.
That means:
- use
projectServiceproject-info helpers for those fields when a project is open - keep app-level
projectEntriesas duplicated cached listing data only - do not treat repository state as the source of truth for project info
These are the current patterns we want new work to align with. They are more implementation-shaped than the stable boundaries above, so they may evolve over time, but they are the current standard.
Resource pages should stay explicit at the page level.
Do not hide an entire resource page behind one giant page factory or one universal layout abstraction.
Preferred structure:
- page YAML stays in
src/pages/<page>/ - reusable center-pane UI lives in
src/components/ - shared page-family orchestration lives in
src/internal/ui/resourcePages/
Current resource page families:
src/components/mediaResourcesView/src/internal/ui/resourcePages/media/src/components/catalogResourcesView/src/internal/ui/resourcePages/catalog/
Current custom resource center components:
src/components/charactersResourcesView/src/components/textStyleResourcesView/
Shared page-family helpers may own:
- repeated store shape
- repeated handler wiring
- shared selection/search/edit orchestration
They must not absorb:
- route structure
- page-specific overlays and dialogs
- file picking and uploads
- repository mutations
- project rules that belong in services or
src/internal/project/
Resource center components must stay presentational.
Current scene-editing pattern:
src/primitives/editableText.jslow-level contenteditable/caret behaviorsrc/components/linesEditor/UI-only editing surfacesrc/internal/ui/sceneEditor/scene-editing workflows and line view-model shapingsrc/pages/sceneEditor/page orchestration, preview/canvas, dialogs, and asset loading
Keep linesEditor in its fixed Rettangoli component files.
Do not add ad hoc sibling helper files inside the component folder when the
logic is really scene-editing orchestration.
linesEditor must not own:
- repository or domain reads
- project service orchestration
- split/merge/create/delete persistence workflows
- scene-specific badge/preview shaping
Scene and preview asset loading has a performance-sensitive contract.
- Keep image and video assets URL-backed when possible.
- Do not regress to fetching large image/video files into JS
ArrayBuffermemory first and then wrapping them intoBlobURLs unless there is a strong technical reason. - The canonical normalization layer for this is
createAssetBufferManager()in theroute-graphicspackage. - The canonical runtime loader behavior is
RouteGraphics.loadAssets()in theroute-graphicspackage.
Current expectation:
- images and videos prefer direct source URLs
- audio stays buffer-backed because it still needs decode/loading behavior
- fonts stay buffer-backed because they are registered through
FontFace
Why this matters:
- the old
URL -> ArrayBuffer -> Blob -> HTMLVideoElementpath caused large JS-side duplication before WebView2/Pixi video decode even began - direct URL-backed image/video loading significantly reduced scene-editor memory spikes
If this asset-loading behavior changes, document the reason in the same PR and re-check scene-editor memory behavior before merging.
src/deps/services/web/collabBootstrapService.js is a web-runtime composition
layer only.
It may:
- create the web project service
- create the collab connection runtime
- publish normalized remote collab events
- expose debug helpers when enabled
It must not:
- scan the DOM for mounted pages
- know page tags or page handler names
- call page handlers directly