Architecture

This page describes the file organization of a SmartMaker application. Unlike a regular React project, file location has technical consequences: it determines the ability to extract a feature to another application, and a poor database layer split causes a loading error that is difficult to diagnose.

Overview

mobile/
├── public/
│   ├── images/              # PWA icons and static images
│   └── locales/
│       └── <lang>/
│           └── <feature>.json   # one file per language AND per feature
├── src/
│   ├── api/
│   │   ├── index.js
│   │   └── mapping/
│   │       └── <feature>.js     # backend <-> front mapping, one per feature
│   ├── db/
│   │   ├── index.js             # Db instantiation
│   │   └── stores/
│   │       └── <feature>/
│   │           ├── indexes.js   # Dexie schema, NO imports
│   │           ├── useDb<Feature>.jsx   # business CRUD hook
│   │           └── index.js     # barrel
│   ├── components/
│   │   ├── app/                 # Router, Provider, Head, Toaster
│   │   ├── layouts/             # shared layouts
│   │   ├── global/              # cross-cutting application components
│   │   └── pages/
│   │       ├── public/<Page>/   # unauthenticated pages
│   │       ├── private/<Page>/  # authenticated pages
│   │       └── errors/<Page>/
│   ├── global-state/
│   │   └── slices/              # UI state only
│   ├── hooks/                   # cross-cutting application hooks
│   ├── i18n/
│   │   └── index.js
│   ├── utils/
│   │   ├── constants/
│   │   ├── functions/
│   │   └── maps/
│   │       ├── form.jsx         # field type -> input component
│   │       └── list.jsx         # column type -> cell component
│   ├── assets/                  # files handled by compilation
│   ├── appConfig.js
│   ├── main.jsx                 # entry point
│   └── sw.js                    # Service Worker (injectManifest mode)
├── .env                         # environment variables, not versioned
├── .env.example
├── index.html
├── eslint.config.js
├── package.json
└── vite.config.js

An application generated by SmartBoot comes with this tree structure already in place, with an example for each location: a complete users store (indexes.js and useDbUsers), a commented api/mapping/, the maps form.jsx and list.jsx, and five translation namespaces. You fill it, you don't have to create it.

The Guiding Principle: One Feature, One Folder

A business feature (interventions, tasks, quotes, inventory) must be able to be copied as-is to another SmartMaker application. In concrete terms, it is distributed across three locations and three only:

Location Content
db/stores/<feature>/ Dexie schema and CRUD hook
api/mapping/<feature>.js conversion between backend and front format
locales/<lang>/<feature>.json translations for the feature namespace

The test is simple: copy these three elements to another project, add the route, and it should work without any other modification. If an import "leaks" to a file specific to the original application, it's a flaw to be corrected.

The Data Layer

All CRUD in One Hook

Each feature exposes a useDb<Feature> hook that carries all local database accesses.

// src/db/stores/interventions/useDbInterventions.jsx
import { db } from "src/db";

export const useDbInterventions = () => {
  const list = async (filters) => { /* ... */ };
  const get = async (id) => { /* ... */ };
  const create = async (payload) => { /* ... */ };
  const update = async (id, payload) => { /* ... */ };
  const remove = async (id) => { /* ... */ };

  return { list, get, create, update, remove };
};

The hook only knows about the Db class, useGlobalStates and useApi. Nothing else from the host project.

Warning

Never put Dexie calls directly in a page component. This is the most common anti-pattern, and it makes a feature impossible to extract later.

The hook name follows the form useDb<Feature>: useDbInterventions, useDbTasks, useDbProducts. Forms like use<Feature>Services found in older projects are legacy to be renamed.

The indexes.js File, and Why It's Mandatory

Important

The Dexie schema for each feature must live in an indexes.js file with no imports, and db/index.js must import it directly, without going through the barrel.

Without this separation, the import graph forms a cycle:

db/index.js
  -> import { tasksIndexes } from "./stores"        (barrel)
  -> stores/tasks/index.js
       export const tasksIndexes = "..."
       export * from "./useDbTasks"                 <- pulls the hook
  -> useDbTasks.jsx
       import { db } from "src/db"                  <- back to start

At compile time, this cycle is partially unwound by Vite and produces a Cannot access 'tasksIndexes' before initialization error when loading the bundle. The symptom is cruel: npm run build succeeds, because it doesn't load the bundle, and the application crashes on startup with an error that seems to come from elsewhere (broken authentication, login loop).

The solution, which breaks the cycle by making indexes.js a leaf in the graph:

// src/db/stores/tasks/indexes.js -- ZERO import
export const tasksIndexes = "++id, ref, status, updatedAt";
// src/db/index.js -- imports LEAFS, not the barrel
import { tasksIndexes } from "./stores/tasks/indexes";
import { projectsIndexes } from "./stores/projects/indexes";

export const db = new Db({
  name: "myapp",
  version: 1,
  stores: { tasks: tasksIndexes, projects: projectsIndexes },
}).db;

export * from "./stores";   // acceptable here: db is already built

The barrel stores/<feature>/index.js remains normally usable by pages and other hooks. Only db/index.js must shortcut.

Where Data Goes

Nature Location
persisted business data (interventions, quotes, photos) Db (Dexie), via useDb<Feature>
persistent UI state (theme, language, filters, last page) useGlobalStates (Redux and redux-persist)
ephemeral UI state (form in progress, open modal) useState or useStates local

Warning

Never place a list of business objects in a Redux slice. It must live in Dexie, and components read it via the feature's hook.

Organizing Components

This is the most frequently asked question. The classification rule is one sentence: we classify a component according to its scope, not according to its subject.

Folder What to put there Membership test
components/app/ application plumbing: Router, provider composition, Head, Toaster there is only one instance in the application
components/layouts/ layouts shared by multiple pages it wraps pages, it is not one
components/global/ cross-cutting components reused by multiple pages it is imported by at least two pages
components/pages/<visibility>/<Page>/ a page, mounted on a route it corresponds to a Router entry

Pages are then organized by visibility: public/ for what is accessible without authentication, private/ for the rest, errors/ for error pages. This split is not decorative: it reflects what the RouteGuard protects and immediately makes visible which page is exposed.

One Component, One Folder

Each component has its own folder with an index.jsx, even if it's alone. This allows later adding, in the same place, its styles, sub-components and tests, without moving or correcting a single import.

components/pages/private/InterventionPage/
├── index.jsx          # the page
├── Header/
│   └── index.jsx      # sub-component specific to this page
└── LinesTable/
    └── index.jsx

Tip

A sub-component used by only one page remains in that page's folder. It only moves to components/global/ when a second page uses it. Moving in anticipation clutters the common space with components that are not shared by anyone.

A Page Should Know Nothing About the Application

A business page must be able to be mounted in the router of another application without modification.

To do:

  • receive its dependencies through SmartCommon hooks (useNavigation, useApi, useGlobalStates) or through its props
  • pass form components through the utils/maps/form.jsx map, so that the host application can override them by field type
  • expose navigation paths in a function, rather than hardcoding navigate('/interventions/123')

Not to do:

  • import appConfig from a business page
  • read a Redux slice specific to the host application

Translations: One Namespace per Feature

public/locales/
├── fr/
│   ├── common.json
│   ├── interventions.json
│   └── products.json
└── en/
    ├── common.json
    ├── interventions.json
    └── products.json
const { t } = useTranslation('interventions');

All namespaces must be declared in the i18next configuration to be preloaded: this is essential for offline operation, as the Service Worker embeds them via its **/*.json pattern.

Warning

Without Suspense, add react: { useSuspense: false } to the i18next configuration. Otherwise, a page reached by direct navigation before the end of loading its namespace remains frozen on raw keys.

The keyPrefix should never be hardcoded in a reusable hook: it is received as a parameter, with a default value.

// To avoid
const useIntStatuses = () => useTranslation(undefined, { keyPrefix: 'intStatuses' });

// Preferred
const useIntStatuses = (keyPrefix = 'intStatuses') => useTranslation(undefined, { keyPrefix });

Configuration: No Hardcoded Values

Everything that changes from one project or environment to another goes through a Vite environment variable or a provider prop.

Variable Role
VITE_API_URL Backend URL; forced relative in production build
VITE_APP_NAME application name
VITE_APP_VERSION version and build number, injected by Makefile
VITE_LOCALES list of languages, derived from subdirectories of public/locales/

appConfig.js can centralize these values, but should only be imported from the application shell, never from a business feature.

Files at the Root of mobile/

File Role
.env environment variables, ignored by Git
.env.example template for .env, versioned
index.html page where React mounts the application; carries the link to the manifest
vite.config.js Vite configuration, including PWA
eslint.config.js lint configuration
package.json dependencies and scripts

Review Checklist

To be completed before opening a merge request, or when taking over an existing project:

  • [ ] all business CRUD is in useDb<Feature>, no Dexie calls in a page
  • [ ] each db/stores/<feature>/ has its indexes.js without imports, and db/index.js imports the leaves
  • [ ] the backend/front mapping is isolated in api/mapping/<feature>.js
  • [ ] no business data in a Redux slice
  • [ ] no business page imports appConfig or a slice specific to the application
  • [ ] one i18n namespace per feature, all declared in the configuration
  • [ ] no fetch or axios: everything goes through useApi
  • [ ] no hardcoded URLs or ports
  • [ ] one component, one folder; sub-components stay with their page as long as they only serve it

See Also