---
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/<app>-web` — one web app per business app):
```
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.

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