---
name: conventions
description: SmartStack global conventions — DB schemas, prefixes, namespaces
group: E
allowed-tools: [Read, Glob, Grep]
---

# Skill: SmartStack Conventions

> Global conventions: DB schemas, prefixes, namespaces.

## DB Schemas

| Schema | Usage |
|--------|-------|
| `core` | SmartStack platform tables |
| `extensions` | Client/extensions tables |

## Domain Table Prefixes

| Prefix | Domain |
|--------|--------|
| `auth_` | Authorization (Users, Roles, Permissions) |
| `nav_` | Navigation (Applications, Modules, Sections) |
| `usr_` | User profiles |
| `ai_` | AI features |
| `cfg_` | Configuration |
| `wkf_` | Workflows |
| `support_` | Support tickets |
| `ref_` | References |
| `loc_` | Localization |
| `lic_` | Licensing |
| `tenant_` | Multi-tenancy |

Custom applications: 2-5 letter prefix (`crm_`, `rh_`, `fi_`).

## Namespaces

> Solution root = `<ns>` (the .NET project prefix, e.g. `Acme`); business app =
> `<App>` (PascalCase of the BA application code, e.g. `Crm`). ONE solution hosts
> MANY apps. The root (`<ns>.Domain`, `<ns>.Api`, …) is fixed — only the suffix
> carries the `<App>.<Module>` classification (mirrors the seeding layer's
> `Applications/<App>/`). Folder == namespace, EXCEPT the Domain layer (see note).
> Single source of truth: `lib/app-classification.ts`.

```
<ns>.Application.<App>.<Module>.{DTOs|Commands|Queries|Handlers|Validators|Interfaces}
<ns>.Application.<App>.<Module>.DTOs.Screens
<ns>.Infrastructure.Services.<App>.<Module>
<ns>.Api.Controllers.<App>.<Module>                 // integration controller (entity-grained)
<ns>.Api.Controllers.<App>.<Module>.<Section>       // screen controller (section-grained)
<ns>.Api.Permissions.<App>.<Module>                 // type <Module>Permissions { class <Section> { … } }
```

Business partition (App → Module → Section) is OUTER; technical sub-buckets
(DTOs/Commands/Screens) are INNER. Section is a folder ONLY where the artifact is
section-grained: screen controllers, acceptance tests, frontend pages.

> **Domain exception — folder moves, namespace stays flat.** Entities, domain
> events and EF configurations move into `<App>/<Module>/` FOLDERS but KEEP the
> flat namespaces `<ns>.Domain.Entities` and
> `<ns>.Infrastructure.Persistence.Configurations`. A cross-module FK config emits
> `builder.HasOne<TargetEntity>()` and must resolve every entity through one
> `using <ns>.Domain.Entities;` — the scaffolder doesn't know a cross-module
> target's module.

Tests mirror the code: `Tests/<App>/<Module>/{Domain|Application|Api|…}/`;
acceptance tests `Tests/<App>/<Module>/<Section>/`. The test-project root stays
`<Prefix>.Tests` (solution prefix for layer tests, business app for AC tests).

Frontend (`web/<project>-web` — ONE web app per PROJECT, hosting every business application; `<app>` is a folder segment inside it, never a web app of its own):
```
src/pages/<app>/<module>/<section>/…      // section-grained UI
src/features/<app>/<module>/<entity>/…    // entity-grained API client
```

The `<App>` business-app code is passed as `applicationCode` (kebab) to the
backend + test scaffolders — DISTINCT from `appCode`/`namespace` (the solution
prefix). `scaffold-screen-controller` / `scaffold-tests-from-ac` already receive
the business app as their `appCode`. URLs, permission keys and componentKeys are
independent of this tree and DO NOT change.

## Service Naming

| Type | Pattern |
|------|---------|
| Interface | `I{Resource}Service` |
| Implementation | `{Resource}Service` |
| Repo interface | `I{Resource}Repository` |
| Repo implementation | `{Resource}Repository` |

## Migrations

Format: `{context}_v{version}_{seq}_{Description}`

## Menu access vs data access (three authorization axes)

Reference: `docs/architecture/menu-vs-data-access.md` in SmartStack.app.

| Axis | Question | Mechanism |
|------|----------|-----------|
| Nav visibility | does the node show in MY menu? | **`access` IS the visibility lock (SmartStack ≥ 3.62)** — a module/section shows (and its page routes) ONLY with `{path}.access` exact, a DESCENDANT's `.access` (a granted child reveals its ancestors), or a covering wildcard (`HasNavAccessToPath` backend / `hasNavAccess` frontend). Data actions (`read`, `create`, `lookup`, …) NEVER reveal a menu node. Exemptions: `IsOpen` / `IsPersonal` apps; resources keep the legacy any-perm rule. |
| Data capability | may I do this TYPE of action? | permission path on the endpoint (`RequirePermission`) — since the `access` lock, data grants no longer need a nav-decoupled prefix to stay out of the menu |
| Row perimeter | on WHICH rows? | data scopes (`read` scoped vs `read.all`) — see below |

**Reference rule (`lookup`)**: when module B needs module A's data as reference
data (FK dropdowns), grant the PRODUCER's `lookup` — generated `/lookup`
endpoints are gated `[RequirePermission(x.lookup, x.read)]` (ANY semantics):
id+name pairs only, no list/detail surface, no menu node. In BA-generated
projects these grants are DERIVED from the data model's FKs
(`derive-lookup-grants` → machine-owned block of `rbac.md`) — never authored
by hand. Rich assignment surfaces keep the consumer-side endpoint gated by the
CONSUMER's permission (`users/assignable-roles` ←
`administration.users.assign`). Granting the producer's `.read` remains for
broad, assumed cross-module needs only — since 3.62 it no longer leaks the
menu, but it still opens the whole read surface.

## Row-level data scopes (RBAC `.read.all` convention)

The platform's row-level security (own/assigned vs all — generalized from the
api.accounts pilot; reference: `docs/architecture/data-scopes-rbac.md` in
SmartStack.app) rests on a NAMING CONVENTION — never break it:

| Permission | Meaning |
|------------|---------|
| `{path}.read` | Read CAPABILITY — row-level **scoped by default** when the entity declares a `DataScopePolicy` (perimeter = own ∪ assigned) |
| `{path}.read.all` | Global row-level visibility for that entity — implied by `{module}.*` / `{app}.*` / `*` via the standard matcher |

Rules:
- A data-scoped module ALWAYS seeds the **sibling pair** `{path}.read` +
  `{path}.read.all` on the same module/section node. The admin UI infers the
  "scoped vs global" badge from the presence of that pair — no backend field.
- EF named query filters (EF 10): `"Tenant"` + `"DataScope"` on scoped
  entities. Lift selectively with `IgnoreTenantScope()` / `IgnoreDataScope()`;
  the parameterless `IgnoreQueryFilters()` is FORBIDDEN on data-scoped entities
  (it drops the security filter).
- Adoption = policy + seed `.read.all` + grandfathering (roles holding `.read`
  receive `.read.all` in the same seed migration → zero behavior change day 1)
  + `RequireDataScope` instance guards.
- BA phase: Portée `own`/`assigned`/`all` map onto this mechanism
  (`/ba-create-rbac` materialization rule; audited by RBAC-006/007).
- Scope TIERS are PATH SUFFIXES, never enum actions: `.read.all` is the only
  5-segment permission form today (its `Action` column stays `Read`); team
  tiers arrive with the future HR application, as additional suffixes.

## Sensitive data (entity grain — the `**Sensibilité**` line)

Reference: `lib/ba-sensitivity.ts` (SSOT) and
`business-analyse/create-data-model/levels/attributes.md` § Sensibilité.
Declared ON THE ENTITY block of `entité.md`, never on an attribute column:

```markdown
- **Sensibilité** : <niveau> — <catégorie>[, <catégorie>…][ — décision client du AAAA-MM-JJ : <raison>]
```

Document default before the first `### ENT-` heading (inherited, like
`**Portée**`); a `composant` inherits its aggregate root; absent = `interne`;
`**Personne**` implies the category `personnel`, never a level.

<!-- sensitivity-vocab:v1 -->
| Niveau | Rang | Signification |
|---|---|---|
| `public` | 0 | exposable à des tiers sans décision supplémentaire (API externe, endpoint anonyme) — jamais une donnée personnelle ni un attribut sensible |
| `interne` | 1 | défaut quand rien n'est écrit — accessible aux acteurs de l'application selon rbac.md, aucun garde-fou supplémentaire |
| `confidentiel` | 2 | accès justifié par un cas d'usage (besoin d'en connaître), grants lookup dérivés à valider par un humain, API externe sur décision client datée |
| `restreint` | 3 | nominatif — jamais d'API externe, jamais d'acteur externe, jamais la recherche globale |

| Catégorie | Libellé | Base légale | Exemples |
|---|---|---|---|
| `personnel` | Données personnelles | nLPD art. 5 let. a / RGPD art. 4 | identité, coordonnées, date de naissance, numéro AVS |
| `personnel-sensible` | Données personnelles sensibles | nLPD art. 5 let. c / RGPD art. 9 | opinions religieuses, philosophiques, politiques ou syndicales, origine raciale ou ethnique, sphère intime, données génétiques ou biométriques, mesures d'aide sociale |
| `sante` | Santé | nLPD art. 5 let. c / RGPD art. 9 | diagnostic, certificat médical, motif d'absence maladie, handicap, grossesse |
| `juridique` | Juridique | nLPD art. 5 let. c (poursuites et sanctions) / secret professionnel | poursuites ou sanctions pénales et administratives, dossier disciplinaire, litiges, casier |
| `rh` | Ressources humaines | CO art. 328b / nLPD | contrat de travail, évaluation, entretien annuel, motifs d'absence, mesures RH |
| `financier` | Financier | CO art. 958f / secret d'affaires | rémunération, fiche de paie, coordonnées bancaires (IBAN), comptabilité |
| `commercial` | Secret d'affaires | LCD art. 6 / secret d'affaires | marges, prix d'achat, conditions fournisseurs, pipeline stratégique |
| `securite` | Sécurité | OWASP / secret technique | mots de passe, clés, jetons, secrets — ne devraient JAMAIS être dans une entité métier (le socle les protège via IDataProtector) |
<!-- /sensitivity-vocab:v1 -->

**The split doctrine.** The platform has NO field-level masking and the
generated `ListDto` / `DetailDto` carry every user field: confidentiality is
achieved by SPLITTING the sensitive attributes into a dedicated entity with
its own section + permission family (the socle's own `HrEmployee` vs
remuneration / payroll / contract tables, locked by fail-closed query
filters). A sensitive-looking attribute inside an `interne` entity is a
modeling defect (DM-037), not a tag to add on the column.

What the tag DRIVES — all deterministic:

| Surface | Guardrail |
|---|---|
| `rbac.md` human rows | RBAC-011 need-to-know: `read` / `read.all` / `export` / `lookup` on a section bound to a ≥ `confidentiel` entity requires a use case of that section for the actor (4th cell `décision client : <raison>` silences the warn); an `external` actor on `restreint` = err |
| derived lookup grants | RBAC-012: `derive-lookup-grants` emits NO automatic `lookup` grant on a sensitive producer — the candidates are listed « à valider », a HUMAN `lookup` / `read` row on the producer section IS the validation |
| cross-application reuse | CODE-009: a FK `scope cross-module (B/MOD)`, a 360 tab or a 4-segment permission from application A onto a ≥ `confidentiel` entity of application B needs a `REQ-NNN` of nature `accès` in `<APP>/demande-extension.md` (carrying the owner's level; same Studio validation, same preflight freeze as an extension request) |
| 360 related tabs | XD-009: explicit backticked permission + a list pagespec target — the « first 4 attributes of entité.md » fallback is refused on a sensitive target |
| generated code | list search `LIKE` on the display field ONLY, global search `.SearchIn(display)` (`restreint` excluded), lookup label never a sensitive attribute, `[AllowAnonymous]` only on a `public` entity (DEV-API-035) |
| public API | `**API externe**` refused on `restreint`; on `confidentiel` only with a dated `décision client` facet on that line (DM-038 / DEV-XAPI-015) |
| documentation | Section 2 « Accès & rôles » states the module's level and categories — nothing more |

What it does NOT do — never promise it in a skill: no field-level masking,
no audit of READS (the platform traces writes, exports, access denials and
impersonation sessions only), no step-up re-authentication on read, no log
masking, no column encryption. Those are platform evolutions
(`development/backend/data-layer/references/data-sensitivity.md`).

## File / document storage

The platform ships the storage PRIMITIVE — never rebuild it, never store file
content in the database:

- **Service**: `IFileStorageService` (`SmartStack.Application.Common.Interfaces`)
  — `UploadAsync` (returns an OPAQUE stored key) / `DownloadAsync` /
  `GetSecureUrlAsync` / `DeleteAsync` / `ExistsAsync` / `GetMetadataAsync`.
  Registered Scoped via `AddSmartStack` → injectable from any extension
  handler/controller.
- **Tiers**: `StorageType.Normal | Legal` — `Legal` is immutable with legal-hold
  retention (Art. 958f CO, 10 years); `DeleteAsync` throws on `Legal`.
- **Config** (shipped in every generated `appsettings.json`): provider chosen by
  `AzureStorage:UseAzure`; Local requires `FileStorage:BasePath` (`ss dev`
  seeds a dev path). The LOCAL implementation validates NOTHING (no size, no
  extension) — validation always lives in the extension controller.
- **Extension pattern** (full reference:
  `development/backend/data-layer/references/file-storage.md`):
  1. client METADATA entity in `extensions.*` — `FileName`, `StoredFileName`
     (opaque, unique, never in DTOs), `ContentType`, `FileSizeBytes` + parent FK;
  2. dedicated endpoints — `IFormFile` upload (`[RequestSizeLimit]` +
     extension/size whitelist in the controller + `[RequirePermission]`),
     AUTHENTICATED streamed download via the tenant-filtered metadata query;
  3. client posts `FormData` via the package's `api` (multipart interceptor);
  4. local dropzone UI (the npm package exports no upload component).
- **Forbidden**: `binary`/`varbinary`/`byte[]` content in the DB; raw
  `System.IO` writes; routing business documents through the socle's
  `api/files/*` (its `normal` download route is ANONYMOUS — branding-asset
  capability URLs only); custom actions with `payloadParameters[].type: 'file'`.

## Frontend

- Layout: no `max-w-*`, standard padding `lg:px-10`
- Tabs: `useTabNavigation` hook, URL sync `?tab=name`, lazy load
- Breadcrumb: `<Breadcrumb items={[...]}/>` as first element
