create-mercato-app 0.6.1-develop.3090.06ab462170 → 0.6.1-develop.3102.d6e7e6d57a

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  # Agent Context Routing — {{PROJECT_NAME}}
2
2
 
3
- **MANDATORY CONTEXT LOADING** — see Critical Rule #5 below.
3
+ **MANDATORY CONTEXT LOADING** — see the "BEFORE writing ANY code" Critical Rule below.
4
4
  Before writing code, find your task below and `Read` the listed files.
5
5
  Do NOT load the entire src/ tree — Open Mercato apps can have many modules.
6
6
 
@@ -51,7 +51,8 @@ step, you WILL produce incorrect imports and miss required patterns.
51
51
  | Add caching | `.ai/guides/cache.md` |
52
52
  | Add background workers | `.ai/guides/queue.md` |
53
53
  | Use i18n (translations) | `.ai/guides/shared.md` → i18n |
54
- | Use encrypted queries | `.ai/guides/shared.md` → Encryption |
54
+ | Use encrypted queries (read sensitive columns that already have an encryption map; for authoring a NEW sensitive column see the row below first) | `.ai/guides/shared.md` → Encryption |
55
+ | **Encrypt sensitive / GDPR-relevant fields** ("we need this column encrypted", "store this securely", "this is PII", "GDPR", "encryption at rest", addresses, contact info, free-text notes about people, integration credentials, secrets) — declare them in the framework's encryption-maps mechanism, never hand-rolled AES/KMS | `.ai/skills/data-model-design/SKILL.md` → Sensitive Data and Encryption Maps, then `.ai/skills/module-scaffold/SKILL.md` → Encryption maps. Reference: <https://docs.open-mercato.dev/user-guide/encryption> |
55
56
  | Use apiCall / UI components | `.ai/guides/ui.md` |
56
57
  | Add permissions (RBAC) | `.ai/guides/core.md` → Access Control |
57
58
  | Add notifications | `.ai/guides/core.md` → Notifications |
@@ -130,11 +131,35 @@ src/modules/<id>/
130
131
  ├── ce.ts # Custom entities / custom field sets
131
132
  ├── translations.ts # Translatable fields per entity
132
133
  ├── notifications.ts # Notification type definitions
133
- └── notifications.client.ts # Client-side notification renderers
134
+ ├── notifications.client.ts # Client-side notification renderers
135
+ └── encryption.ts # Tenant data encryption maps (defaultEncryptionMaps) for sensitive / GDPR fields
134
136
  ```
135
137
 
136
138
  Register in `src/modules.ts`: `{ id: '<id>', from: '@app' }`
137
139
 
140
+ ## Mandatory Module Mechanisms (every module MUST use these — no DIY substitutes)
141
+
142
+ When the user asks to **create a new application** or a **new module**, do not invent your own routing, auth, persistence, forms, or caching. The framework provides one canonical primitive for each concern. If a feature is not on this list and not in the Task → Context Map, ask before adding it — do not roll your own.
143
+
144
+ | Concern | Canonical mechanism | Where to learn it |
145
+ |---|---|---|
146
+ | Module structure & auto-discovery | `src/modules/<id>/{api,backend,frontend,data,subscribers,workers,widgets}` + `index.ts` + `src/modules.ts` (`from: '@app'`) — discovered by `yarn generate` | `.ai/skills/module-scaffold/SKILL.md`, `.ai/guides/core.md` → Module Files; <https://docs.open-mercato.dev/framework/modules/overview> |
147
+ | **Backend admin pages** | Auto-discovered files under `src/modules/<id>/backend/**`, paired `page.meta.ts` with `requireAuth` + `requireFeatures` + `pageGroup`/`pageGroupKey`/`pageOrder` | `.ai/skills/backend-ui-design/SKILL.md`, `.ai/skills/module-scaffold/references/navigation-patterns.md`; <https://docs.open-mercato.dev/framework/modules/routes-and-pages> |
148
+ | **Frontend public pages** | Auto-discovered files under `src/modules/<id>/frontend/**`. Customer portal pages live under `frontend/[orgSlug]/portal/<path>/page.tsx` with `requireCustomerAuth` / `requireCustomerFeatures` in `page.meta.ts` | `.ai/guides/ui.md` → Portal Extension; <https://docs.open-mercato.dev/framework/modules/routes-and-pages> |
149
+ | **API routes** | Files under `src/modules/<id>/api/**/route.ts` exporting handlers + `metadata` (per-method `requireAuth` / `requireFeatures`) + `openApi`. NEVER write a top-level `export const requireAuth` — the registry no longer recognises it | `.ai/guides/core.md` → API Routes; <https://docs.open-mercato.dev/framework/api/api-development-guide> |
150
+ | **CRUD APIs (factory)** | `makeCrudRoute({ entity, entityId, operations, schema, indexer: { entityType } })` from `@open-mercato/shared/lib/crud/factory`. Always set `indexer` so query-index coverage stays correct. Custom (non-`makeCrudRoute`) write routes MUST call `validateCrudMutationGuard` before the mutation and `runCrudMutationGuardAfterSuccess` after success | `.ai/skills/module-scaffold/SKILL.md` → Create API Routes; <https://docs.open-mercato.dev/framework/api/crud-factory> |
151
+ | **CRUD forms in admin** | `<CrudForm entityId apiPath mode fields />` from `@open-mercato/ui/backend/CrudForm`; helpers `createCrud` / `updateCrud` / `deleteCrud` from `@open-mercato/ui/backend/utils/crud`; `createCrudFormError` from `@open-mercato/ui/backend/utils/serverErrors`. Never raw `<form>`, never raw `fetch` | `.ai/skills/backend-ui-design/SKILL.md`; <https://docs.open-mercato.dev/framework/admin-ui/crud-form> |
152
+ | **DataTables in admin** | `<DataTable entityId apiPath title columns />` from `@open-mercato/ui/backend/DataTable`. Keep `entityId` and `extensionTableId` stable so widget injection (columns, row actions, bulk actions, filters, toolbar) keeps working | `.ai/skills/backend-ui-design/SKILL.md`; <https://docs.open-mercato.dev/framework/admin-ui/data-grids> |
153
+ | **Authorization (RBAC)** | Declare features in `<module>/acl.ts`, grant them in `<module>/setup.ts` `defaultRoleFeatures`, gate pages and routes with `requireFeatures` in `metadata` / `page.meta.ts`. NEVER use `requireRoles` (role names mutate). Run `yarn mercato auth sync-role-acls` after adding new features | `.ai/guides/core.md` → Access Control; <https://docs.open-mercato.dev/framework/rbac/overview> |
154
+ | **Multi-tenant scoping (default for every entity)** | Every tenant-scoped entity MUST include indexed `organization_id` and `tenant_id` columns and every read/write MUST filter by them. The CRUD factory injects scope automatically — do NOT bypass it. For ad-hoc queries use `withScopedPayload` from `@open-mercato/shared/lib/api/scoped` | `.ai/skills/data-model-design/SKILL.md`; <https://docs.open-mercato.dev/architecture/system-overview> |
155
+ | **Encryption maps for sensitive data** | Declare a module-level `<module>/encryption.ts` exporting `defaultEncryptionMaps: ModuleEncryptionMap[]` from `@open-mercato/shared/modules/encryption`. Read encrypted columns via `findWithDecryption` / `findOneWithDecryption` from `@open-mercato/shared/lib/encryption/find`. NEVER hand-roll AES/KMS, NEVER use `em.find` on encrypted columns | `.ai/skills/data-model-design/SKILL.md` → Sensitive Data and Encryption Maps; <https://docs.open-mercato.dev/user-guide/encryption> |
156
+ | **Cache** | Resolve the cache from DI (`container.resolve('cache')`) — never `new Redis(...)` or raw SQLite. Tag with `tenant:<id>` / `org:<id>` and the entity-type tag so invalidation stays tenant-scoped | `.ai/guides/cache.md`; <https://docs.open-mercato.dev/user-guide/cache-management> |
157
+ | **Background workers** | `src/modules/<id>/workers/*.ts` exporting `metadata: { queue, id?, concurrency? }` + default handler. Never spin up custom queues | `.ai/guides/queue.md`; <https://docs.open-mercato.dev/framework/events/queue-workers> |
158
+ | **Events between modules** | `<module>/events.ts` with `createModuleEvents({ moduleId, events } as const)`. Subscribers in `subscribers/*.ts`. Never call other modules' services directly across module boundaries | `.ai/guides/events.md`; <https://docs.open-mercato.dev/framework/events/overview> |
159
+ | **i18n (every user-facing string)** | `useT()` client-side from `@open-mercato/shared/lib/i18n/context`, `resolveTranslations()` server-side from `@open-mercato/shared/lib/i18n/server`; keys in `src/i18n/<locale>.json`. Never hard-code labels in components | `.ai/guides/shared.md` → i18n |
160
+
161
+ > Rule of thumb: if you find yourself reaching for raw `fetch`, raw `<form>`, ad-hoc `crypto`, ad-hoc `Redis`, or a manual `m2m` join across modules, stop and check the row above — there is a canonical helper.
162
+
138
163
  ## CRITICAL rules — always follow without exception
139
164
 
140
165
  1. **Entity classes live in `src/modules/<module>/data/entities.ts` and MUST import decorators from `@mikro-orm/decorators/legacy`.** Start there for every schema change.
@@ -156,9 +181,16 @@ Register in `src/modules.ts`: `{ id: '<id>', from: '@app' }`
156
181
  **Dashboards fallback rule.** When the user (or the `trim-unused-modules` skill) disables the `dashboards` module, you MUST update `src/app/(backend)/backend/page.tsx` so it no longer renders `<DashboardScreen />`. Replace the dashboard render with a `redirect(...)` to the first enabled backend page for the current user — preferring pages already registered in the main sidebar group and respecting the admin/superadmin role of the caller. Otherwise `/backend` will crash at build or request time because the removed module no longer ships `DashboardScreen`. Always fall back to `/backend/profile` only if no other backend page is available.
157
182
  9. **New features MUST be visible to default roles immediately.** Every time you add a new feature ID (e.g. `my_module.view`, `my_module.manage`) to `src/modules/<module>/acl.ts`, you MUST also (a) add that feature to `defaultRoleFeatures` in the same module's `setup.ts` so the admin role and any other appropriate default roles get it on every tenant setup; and (b) run `yarn mercato auth sync-role-acls` so existing tenants pick up the new feature without a reinstall. Use `--tenant <tenantId>` only when the user asks to target one tenant. Do this automatically unless the user has explicitly said otherwise — the user should see the features you are building, not stare at a blank admin because their role is missing a grant. Feature IDs are FROZEN once shipped; if a rename is required, add the new ID alongside, grant it, and keep the old one as a deprecated alias.
158
183
  10. **Strict Design System alignment for every UI change.** Any UI you add or edit MUST use the Open Mercato design system components and tokens. No hardcoded Tailwind status colors (`text-red-500`, `bg-green-100`, etc.) — use semantic tokens (`text-status-error-text`, `bg-status-success-bg`, …). No arbitrary text sizes (`text-[11px]`, `text-[13px]`) — use the Tailwind scale (`text-xs`, `text-sm`, `text-base`, `text-lg`, `text-xl`, `text-2xl`) or the `text-overline` token for 11px uppercase labels. In PAGE BODY UI, use `lucide-react` icons (never inline `<svg>`). Use `StatusBadge` for entity status, `Alert` for inline feedback, `FormField` for standalone form inputs, `SectionHeader` for detail-page section headings, `CollapsibleSection` for collapsible regions, `LoadingMessage`/`Spinner`/`DataLoader` for async states, and `EmptyState` (or DataTable's `emptyState` prop) for empty lists. For list pages, follow `.ai/skills/backend-ui-design/SKILL.md` and prefer the `DataTable` host pattern shown there (`entityId`, `apiPath`, stable `extensionTableId`, and explicit pagination props when you own the data source). Every dialog MUST support `Cmd/Ctrl+Enter` to submit and `Escape` to cancel. Every icon-only button MUST have an `aria-label`. These rules apply to `src/modules/<module>/backend/**` and `src/modules/<module>/frontend/**` alike.
159
- 11. **BEFORE writing ANY code**, you MUST:
184
+ 11. **Sensitive / GDPR fields MUST go through the encryption-maps mechanism — never hand-rolled.** The framework provides per-tenant DEKs, KMS-backed key resolution, and a declarative field-level map. Whenever the user asks for "this field encrypted", "store this securely", "this is PII", "GDPR", "encryption at rest", or you are designing a column that will hold names, addresses, contacts, free-text notes about people, integration secrets/credentials, or any data subject to a data-processing agreement, you MUST:
185
+ - Declare the entity + field list in `src/modules/<module>/encryption.ts` exporting `defaultEncryptionMaps: ModuleEncryptionMap[]` (type imported from `@open-mercato/shared/modules/encryption`).
186
+ - Read those columns via `findWithDecryption` / `findOneWithDecryption` from `@open-mercato/shared/lib/encryption/find` (passing `tenantId` and `organizationId`). Never use raw `em.find` on encrypted columns.
187
+ - For deterministic-lookup fields (e.g., login email), declare a sibling `hashField` in the map so equality lookups still work.
188
+ - Run `yarn mercato entities seed-encryption --tenant <tenantId>` after adding maps so existing tenants pick them up; new tenants get them automatically during `auth:setup`.
189
+ - Treat hand-rolled AES, raw `crypto.subtle`, custom KMS calls, or storing plaintext "for now" as broken — rewrite via the maps. See `.ai/skills/data-model-design/SKILL.md` → Sensitive Data and Encryption Maps and <https://docs.open-mercato.dev/user-guide/encryption>.
190
+ 12. **BEFORE writing ANY code**, you MUST:
160
191
  - Match your task against the **Task → Context Map** above
161
192
  - `Read` every file listed in the "Load" column for your task type
193
+ - Read the **Mandatory Module Mechanisms** section above to confirm which canonical primitives apply (CRUD factory, CrudForm, DataTable, RBAC, multi-tenant scoping, encryption maps, cache, events) — do not invent your own substitutes
162
194
  - Only then proceed to implementation
163
195
  - If your task matches multiple rows, load ALL listed files
164
196
  - **Do NOT skip this step.** The guides contain canonical import paths, required patterns, and conventions that CANNOT be reliably inferred from existing code alone. Skipping leads to wrong imports, missing conventions, and rework.
@@ -197,7 +229,8 @@ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
197
229
  import { apiCall, apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
198
230
 
199
231
  // CRUD forms
200
- import { CrudForm, createCrud, updateCrud, deleteCrud } from '@open-mercato/ui/backend/crud'
232
+ import { CrudForm, type CrudField, type CrudFormGroup } from '@open-mercato/ui/backend/CrudForm'
233
+ import { createCrud, updateCrud, deleteCrud } from '@open-mercato/ui/backend/utils/crud'
201
234
  import { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'
202
235
 
203
236
  // UI components (MUST use — never raw <button>)
@@ -1,9 +1,14 @@
1
1
  import { defineConfig } from '@playwright/test'
2
2
  import path from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
3
4
  import { discoverIntegrationSpecFiles } from '@open-mercato/cli/lib/testing/integration-discovery'
4
5
 
5
6
  const captureScreenshots = process.env.PW_CAPTURE_SCREENSHOTS === '1'
6
7
  const isGitHubActions = process.env.GITHUB_ACTIONS === 'true'
8
+ // Standalone apps generated from this template declare `"type": "module"`,
9
+ // so the CommonJS `__dirname` is undefined when Playwright loads this config
10
+ // under the Node ESM loader. Reconstruct it from `import.meta.url`.
11
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
7
12
  const projectRoot = path.resolve(__dirname, '..', '..', '..')
8
13
  const qaTestResultsRoot = path.join(projectRoot, '.ai', 'qa', 'test-results')
9
14
  const normalizePath = (value: string) => value.split(path.sep).join('/')
@@ -16,7 +16,8 @@ Design entities, relationships, and manage the migration lifecycle following Ope
16
16
  5. [Cross-Module References](#5-cross-module-references)
17
17
  6. [Migration Lifecycle](#6-migration-lifecycle)
18
18
  7. [Advanced Patterns](#7-advanced-patterns)
19
- 8. [Anti-Patterns](#8-anti-patterns)
19
+ 8. [Sensitive Data and Encryption Maps](#8-sensitive-data-and-encryption-maps)
20
+ 9. [Anti-Patterns](#9-anti-patterns)
20
21
 
21
22
  ---
22
23
 
@@ -483,7 +484,89 @@ export class TicketHistory {
483
484
 
484
485
  ---
485
486
 
486
- ## 8. Anti-Patterns
487
+ ## 8. Sensitive Data and Encryption Maps
488
+
489
+ When the developer asks for "we need this column encrypted", "store this securely", "this is PII", "GDPR", or "encryption at rest" — and whenever you are designing a column that will hold names, addresses, contact information, free-text notes about people, integration credentials, secrets, or any data subject to a data-processing agreement — use the framework's **encryption-maps mechanism**. Do NOT hand-roll AES, raw `crypto.subtle`, custom KMS calls, or "TODO encrypt later" stubs.
490
+
491
+ The mechanism gives you:
492
+
493
+ - Per-tenant Data Encryption Keys (DEKs) resolved through the configured KMS (Vault by default, env-fallback in dev).
494
+ - Declarative, per-entity, per-field encryption with optional deterministic-hash sibling columns for equality lookups (for example login by email).
495
+ - Boot-time auto-application: every enabled module's `defaultEncryptionMaps` is collected during `auth:setup` and applied when `TENANT_DATA_ENCRYPTION=yes`.
496
+ - A `findWithDecryption` / `findOneWithDecryption` read API that transparently decrypts on read.
497
+
498
+ ### When encryption is mandatory
499
+
500
+ | Field example | Encrypt? |
501
+ |---|---|
502
+ | First name, last name, preferred name | Yes |
503
+ | Email, phone | Yes — usually with a `hashField` for lookups |
504
+ | Postal address (line 1/2, city, region, postal code, country) | Yes |
505
+ | Free-text comments / notes / activity bodies that mention people | Yes |
506
+ | Integration secrets, API keys, OAuth tokens, webhook signing keys | Yes |
507
+ | Document numbers (tax IDs, national IDs) | Yes |
508
+ | Status enums, counters, timestamps, FKs, currency codes | No |
509
+ | Public catalog metadata (product titles for a public storefront) | Usually no |
510
+
511
+ If you are unsure, default to encrypting and confirm with the user — re-introducing encryption later requires a backfill, but turning it off later is a single map edit.
512
+
513
+ ### Declare the map in `<module>/encryption.ts`
514
+
515
+ ```typescript
516
+ import type { ModuleEncryptionMap } from '@open-mercato/shared/modules/encryption'
517
+
518
+ export const defaultEncryptionMaps: ModuleEncryptionMap[] = [
519
+ {
520
+ entityId: '<module_id>:<entity>', // matches the entity's table id (colon-separated)
521
+ fields: [
522
+ { field: 'first_name' },
523
+ { field: 'last_name' },
524
+ { field: 'phone' },
525
+ // Sibling deterministic hash for equality lookups (e.g. login by email).
526
+ // Add a matching `<field>_hash varchar` column to the entity.
527
+ { field: 'email', hashField: 'email_hash' },
528
+ ],
529
+ },
530
+ ]
531
+
532
+ export default defaultEncryptionMaps
533
+ ```
534
+
535
+ ### Read with decryption — never raw `em.find`
536
+
537
+ ```typescript
538
+ import { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
539
+
540
+ // Signature: (em, entityName, where, options?, scope?). MikroORM FindOptions go in slot 4
541
+ // (pass `undefined` if you have none), the decryption scope `{ tenantId, organizationId }` in slot 5.
542
+ const records = await findWithDecryption(em, '<Entity>', filter, undefined, { tenantId, organizationId })
543
+ const single = await findOneWithDecryption(em, '<Entity>', { id }, undefined, { tenantId, organizationId })
544
+ ```
545
+
546
+ Calling `em.find` on an encrypted column returns ciphertext, breaks search, and silently leaks bug surface. The `findWithDecryption` family is the one entry point.
547
+
548
+ ### Apply maps to existing tenants
549
+
550
+ ```bash
551
+ yarn mercato entities seed-encryption --tenant <tenantId> [--organization <orgId>]
552
+ ```
553
+
554
+ New tenants pick up the maps automatically during `auth:setup`. Toggling the **Encrypted** flag on a custom field via the admin UI also only applies to data written **after** the change — backfill historical plaintext rows by running `yarn mercato entities rotate-encryption-key --tenant <tenantId> --org <organizationId>` (without `--old-key` it skips already-encrypted fields and just encrypts plaintext). Use `yarn mercato entities decrypt-database` to roll back. For full UI flows and CLI options see <https://docs.open-mercato.dev/user-guide/encryption>.
555
+
556
+ ### Vector search caveat
557
+
558
+ The `vector` module stores raw embeddings unencrypted in the vector store (e.g. pgvector). Even though the source text is decrypted only transiently to compute embeddings, treat the embeddings as sensitive: avoid embedding raw high-sensitivity text and rely on disk-level / managed-database encryption-at-rest for the vector column.
559
+
560
+ ### Environment switches
561
+
562
+ - `TENANT_DATA_ENCRYPTION=yes|no` (default `yes`) — set to `no` to run the hooks as no-op (validation still applies).
563
+ - `TENANT_DATA_ENCRYPTION_DEBUG=yes` — log map evaluation, KMS calls, cache hits.
564
+ - `VAULT_ADDR` / `VAULT_TOKEN` / `VAULT_KV_PATH` — HashiCorp Vault KMS configuration.
565
+ - `TENANT_DATA_ENCRYPTION_FALLBACK_KEY` — local/dev fallback key when Vault is unavailable. In dev, `AUTH_SECRET` / `NEXTAUTH_SECRET` is used as a last resort; production falls back to noop KMS.
566
+
567
+ ---
568
+
569
+ ## 9. Anti-Patterns
487
570
 
488
571
  | Anti-Pattern | Problem | Correct Pattern |
489
572
  |-------------|---------|-----------------|
@@ -498,6 +581,10 @@ export class TicketHistory {
498
581
  | Storing arrays as comma-separated strings | Can't query, no integrity | Use `jsonb` arrays or junction tables |
499
582
  | UUID FK without index | Slow joins | Always `@Index()` on FK columns |
500
583
  | Nullable required fields | Data integrity issues | Use `!` assertion for required, `null` for optional |
584
+ | Hand-rolled AES / `crypto.subtle` / custom KMS for sensitive columns | Per-tenant key isolation, hash lookups, key rotation, and admin UI all break | Declare `<module>/encryption.ts` with `defaultEncryptionMaps`; let the framework manage DEKs and Vault |
585
+ | Reading encrypted columns with raw `em.find` / `em.findOne` | Returns ciphertext, breaks search, silent data corruption | Use `findWithDecryption` / `findOneWithDecryption` with `{ tenantId, organizationId }` |
586
+ | Storing PII as plaintext "for now" / TODO comments | GDPR violation, leaks at rest, expensive backfill later | Encrypt from day one; toggling later only protects new writes |
587
+ | Encrypting an `email` column without a `hashField` | Login / equality lookups stop working | Declare a sibling `hashField` (e.g. `email_hash`) in the encryption map and add the matching `varchar` column |
501
588
 
502
589
  ---
503
590
 
@@ -515,7 +602,8 @@ export class TicketHistory {
515
602
  - **MUST** specify `length` on all `varchar` columns
516
603
  - **MUST NOT** use ORM relationship decorators across module boundaries
517
604
  - **MUST NOT** rename or drop columns in a single release
518
- - **MUST NOT** store sensitive data without encryption (use `findWithDecryption`)
605
+ - **MUST** declare encrypted columns in `<module>/encryption.ts` exporting `defaultEncryptionMaps: ModuleEncryptionMap[]`, and read them via `findWithDecryption` / `findOneWithDecryption` from `@open-mercato/shared/lib/encryption/find` — see section 8
606
+ - **MUST NOT** hand-roll AES / KMS calls or store sensitive columns as plaintext "for now" — use the encryption-maps mechanism in section 8
519
607
  - Use `jsonb` for flexible/nested data, proper columns for queryable/sortable data
520
608
  - Use junction tables for many-to-many relationships
521
609
  - Derive TypeScript types from Zod schemas, never duplicate type definitions
@@ -40,14 +40,18 @@ For every piece of code, enforce these code-review rules inline:
40
40
  | Area | Rule |
41
41
  |------|------|
42
42
  | Types | No `any` — use zod + `z.infer` |
43
- | API routes | Export `openApi` and `metadata` with auth guards |
44
- | Entities | Standard columns, snake_case, UUID PKs, `organization_id` + `tenant_id` |
43
+ | API routes | Export `openApi` and per-method `metadata` with `requireAuth` / `requireFeatures` (no top-level `export const requireAuth`) |
44
+ | **CRUD APIs** | **Use `makeCrudRoute({ entity, entityId, operations, schema, indexer: { entityType } })` from `@open-mercato/shared/lib/crud/factory`. Custom write routes MUST call `validateCrudMutationGuard` before the mutation and `runCrudMutationGuardAfterSuccess` after success. See `AGENTS.md` → Mandatory Module Mechanisms.** |
45
+ | Entities | Standard columns, snake_case, UUID PKs, indexed `organization_id` + `tenant_id` |
45
46
  | Security | `findWithDecryption`, tenant scoping, zod validation |
46
- | UI | `CrudForm`/`DataTable`, `apiCall`, `flash()`, `LoadingMessage`/`ErrorMessage` |
47
- | Events | `createModuleEvents()` with `as const`, subscribers export `metadata` |
47
+ | **Encryption maps** | **For every PII / GDPR-relevant column the phase touches, declare in `<module>/encryption.ts` exporting `defaultEncryptionMaps` (type from `@open-mercato/shared/modules/encryption`). Reads via `findWithDecryption` / `findOneWithDecryption` (5-arg `(em, entity, where, options?, scope?)`). Equality-lookup columns declare a sibling `hashField`. NEVER hand-rolled AES/KMS, `crypto.subtle`, or "encrypt later" stubs. See `AGENTS.md` → CRITICAL Rule #11 (Encryption maps) + the "Encryption maps" row of the Mandatory Module Mechanisms table; `.ai/skills/data-model-design/SKILL.md` § Sensitive Data and Encryption Maps; `.ai/skills/module-scaffold/SKILL.md` § Encryption maps.** |
48
+ | UI | `<CrudForm>`/`<DataTable>` (with stable `entityId` + `extensionTableId`), `apiCall` (never raw `fetch`), `flash()`, `<LoadingMessage>`/`<ErrorMessage>` |
49
+ | **Design System** | **Semantic status tokens (no `text-red-*` / `bg-green-*`); Tailwind text scale (no `text-[13px]` / `text-[11px]`); shared primitives `StatusBadge` / `Alert` / `FormField` / `SectionHeader` / `CollapsibleSection` / `LoadingMessage` / `Spinner` / `DataLoader` / `EmptyState`; lucide-react icons in PAGE BODY (never inline `<svg>`); `aria-label` on every icon-only button; Boy Scout rule on touched lines. See `AGENTS.md` → CRITICAL Rule #10 (Strict Design System alignment) + `.ai/skills/backend-ui-design/SKILL.md`.** |
50
+ | **Cache** | **Resolve via DI (`container.resolve('cache')`); tag with `tenant:<id>` / `org:<id>`; declare invalidation per write path. NEVER `new Redis(...)` or raw SQLite.** |
51
+ | Events | `createModuleEvents()` with `as const`, subscribers export `metadata`; cross-module side effects via subscribers, never direct imports |
48
52
  | i18n | `useT()` client, `resolveTranslations()` server, no hardcoded strings |
49
53
  | Imports | Package-level `@open-mercato/<pkg>/...` for framework imports |
50
- | Mutations | `useGuardedMutation` when not using CrudForm |
54
+ | Mutations | `useGuardedMutation` when not using CrudForm; pass `retryLastMutation` in injection context |
51
55
  | Keyboard | `Cmd/Ctrl+Enter` submit, `Escape` cancel on dialogs |
52
56
  | Naming | Modules plural snake_case, events `module.entity.past_tense`, features `module.action` |
53
57
 
@@ -41,8 +41,9 @@ Before writing any code, ask the developer:
41
41
  - [ ] Background workers
42
42
  - [ ] CLI commands
43
43
  - [ ] Custom fields support
44
+ - [ ] **Sensitive / GDPR-relevant fields** (PII, contact info, addresses, free-text notes about people, integration credentials, secrets) — if yes, an `encryption.ts` declaring `defaultEncryptionMaps` is mandatory; see section 11 → Encryption maps
44
45
 
45
- If the developer provides a brief description, infer reasonable defaults and confirm.
46
+ If the developer provides a brief description, infer reasonable defaults and confirm. When key fields include names, emails, phones, addresses, free-text comments, or external API keys, treat the encryption checkbox as `yes` by default and confirm with the user rather than skipping it silently.
46
47
 
47
48
  ---
48
49
 
@@ -57,6 +58,7 @@ src/modules/<module_id>/
57
58
  ├── setup.ts # Tenant init, role features
58
59
  ├── di.ts # Awilix DI registrations
59
60
  ├── events.ts # Typed event declarations (if needed)
61
+ ├── encryption.ts # Tenant data encryption maps (only if entity has sensitive/GDPR fields)
60
62
  ├── data/
61
63
  │ ├── entities.ts # MikroORM entity classes
62
64
  │ └── validators.ts # Zod validation schemas
@@ -178,7 +180,7 @@ Use `makeCrudRoute` for standard CRUD. Each HTTP method lives in its own file.
178
180
  **File**: `src/modules/<module_id>/api/get/<entities>.ts`
179
181
 
180
182
  ```typescript
181
- import { makeCrudRoute } from '@open-mercato/shared/lib/crud/make-crud-route'
183
+ import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
182
184
  import { <Entity> } from '../../data/entities'
183
185
 
184
186
  const handler = makeCrudRoute({
@@ -201,7 +203,7 @@ export const openApi = {
201
203
  **File**: `src/modules/<module_id>/api/post/<entities>.ts`
202
204
 
203
205
  ```typescript
204
- import { makeCrudRoute } from '@open-mercato/shared/lib/crud/make-crud-route'
206
+ import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
205
207
  import { <Entity> } from '../../data/entities'
206
208
  import { create<Entity>Schema } from '../../data/validators'
207
209
 
@@ -225,7 +227,7 @@ export const openApi = {
225
227
  **File**: `src/modules/<module_id>/api/put/<entities>.ts`
226
228
 
227
229
  ```typescript
228
- import { makeCrudRoute } from '@open-mercato/shared/lib/crud/make-crud-route'
230
+ import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
229
231
  import { <Entity> } from '../../data/entities'
230
232
  import { update<Entity>Schema } from '../../data/validators'
231
233
 
@@ -249,7 +251,7 @@ export const openApi = {
249
251
  **File**: `src/modules/<module_id>/api/delete/<entities>.ts`
250
252
 
251
253
  ```typescript
252
- import { makeCrudRoute } from '@open-mercato/shared/lib/crud/make-crud-route'
254
+ import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
253
255
  import { <Entity> } from '../../data/entities'
254
256
 
255
257
  const handler = makeCrudRoute({
@@ -346,7 +348,7 @@ export const metadata = {
346
348
 
347
349
  ```tsx
348
350
  'use client'
349
- import { CrudForm } from '@open-mercato/ui/backend/crud'
351
+ import { CrudForm } from '@open-mercato/ui/backend/CrudForm'
350
352
  import { useT } from '@open-mercato/shared/lib/i18n/context'
351
353
 
352
354
  export default function Create<Entity>Page() {
@@ -384,7 +386,7 @@ export const metadata = {
384
386
 
385
387
  ```tsx
386
388
  'use client'
387
- import { CrudForm } from '@open-mercato/ui/backend/crud'
389
+ import { CrudForm } from '@open-mercato/ui/backend/CrudForm'
388
390
  import { useT } from '@open-mercato/shared/lib/i18n/context'
389
391
 
390
392
  export default function Edit<Entity>Page({ params }: { params: { id: string } }) {
@@ -573,6 +575,52 @@ export default function registerCli(program: any) {
573
575
  }
574
576
  ```
575
577
 
578
+ ### Encryption maps (sensitive / GDPR-relevant fields)
579
+
580
+ **Mandatory** when the entity stores PII, contact info, addresses, free-text notes about people, integration credentials, secrets, or anything subject to a data-processing agreement. Do NOT hand-roll AES, KMS calls, or "TODO encrypt later" stubs — the framework provides per-tenant DEKs and a declarative field-level map.
581
+
582
+ **File**: `src/modules/<module_id>/encryption.ts`
583
+
584
+ ```typescript
585
+ import type { ModuleEncryptionMap } from '@open-mercato/shared/modules/encryption'
586
+
587
+ export const defaultEncryptionMaps: ModuleEncryptionMap[] = [
588
+ {
589
+ entityId: '<module_id>:<entity>', // matches data/entities.ts table id, colon-separated
590
+ fields: [
591
+ { field: 'first_name' },
592
+ { field: 'last_name' },
593
+ { field: 'phone' },
594
+ // Add a hashField for deterministic equality lookups (e.g. login by email):
595
+ { field: 'email', hashField: 'email_hash' },
596
+ ],
597
+ },
598
+ ]
599
+
600
+ export default defaultEncryptionMaps
601
+ ```
602
+
603
+ **Read paths** — never `em.find` an encrypted column directly:
604
+
605
+ ```typescript
606
+ import { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
607
+
608
+ // Signature: (em, entityName, where, options?, scope?) — MikroORM FindOptions in slot 4
609
+ // (pass `undefined` when none), decryption scope in slot 5.
610
+ const records = await findWithDecryption(em, '<Entity>', filter, undefined, { tenantId, organizationId })
611
+ const single = await findOneWithDecryption(em, '<Entity>', { id }, undefined, { tenantId, organizationId })
612
+ ```
613
+
614
+ **Apply to existing tenants** after declaring or updating maps:
615
+
616
+ ```bash
617
+ yarn mercato entities seed-encryption --tenant <tenantId> [--organization <orgId>]
618
+ ```
619
+
620
+ New tenants pick up `defaultEncryptionMaps` automatically during `auth:setup`. Toggling the **Encrypted** flag for a field only applies to data written **after** the change — historical plaintext rows stay as they were until backfilled via `yarn mercato entities rotate-encryption-key --tenant <tenantId> --org <organizationId>` (without `--old-key` the command only encrypts plaintext and skips already-encrypted fields). Use `yarn mercato entities decrypt-database` to roll back. For end-to-end usage and admin UI flows see <https://docs.open-mercato.dev/user-guide/encryption>.
621
+
622
+ > Tip: when `email` (or any other column) needs deterministic lookups while encrypted, declare a sibling `hashField` in the map and add a matching `varchar` column to the entity. The framework keeps the hash in sync on writes; queries can target the hash instead of the cleartext column.
623
+
576
624
  ---
577
625
 
578
626
  ## 12. Wire & Verify
@@ -658,3 +706,5 @@ yarn dev # Start dev server
658
706
  - **MUST NOT** run `yarn db:migrate` without explicit user confirmation
659
707
  - **MUST NOT** create ORM relationships (`@ManyToOne`, `@OneToMany`) to entities in other modules
660
708
  - **MUST NOT** edit `.mercato/generated/*` files manually
709
+ - **MUST** declare `<module>/encryption.ts` exporting `defaultEncryptionMaps` whenever the entity stores sensitive / GDPR-relevant fields (PII, contact info, addresses, free-text notes about people, integration credentials, secrets) — and read those columns via `findWithDecryption` / `findOneWithDecryption`
710
+ - **MUST NOT** hand-roll AES/KMS calls or store "we'll encrypt this later" plaintext for sensitive columns — use the encryption-maps mechanism described in section 11 → Encryption maps
@@ -60,6 +60,9 @@ Use the [Specification Template](references/spec-template.md). Adapt if needed,
60
60
  3. **Singularity Law**: Singular naming for entities, commands, events, feature IDs.
61
61
  4. **Undo Contract**: Is the "Undo" logic as detailed as the "Execute"?
62
62
  5. **Module Isolation**: Using Event Bus for side effects or cheating with direct imports?
63
+ 6. **Canonical Mechanisms**: Does the spec reach for the framework primitives (`makeCrudRoute`, `<CrudForm>`, `<DataTable>`, `apiCall` / `useGuardedMutation`, DI-resolved cache, `createModuleEvents`) or invent its own substitute? See `AGENTS.md` → **Mandatory Module Mechanisms** for the full canon and links. No raw `fetch`, no raw `<form>`, no `new Redis(...)`, no manual cross-module ORM joins.
64
+ 7. **Sensitive Data**: For every PII / GDPR / address / contact / free-text-about-people / integration-credential column the spec proposes, does it declare an `encryption.ts` `defaultEncryptionMaps` entry and route reads through `findWithDecryption`? See `AGENTS.md` → CRITICAL Rule #11 (Encryption maps) + the "Encryption maps for sensitive data" row of the Mandatory Module Mechanisms table and `.ai/skills/data-model-design/SKILL.md` § Sensitive Data and Encryption Maps. No hand-rolled AES, no `crypto.subtle`, no "TODO encrypt later".
65
+ 8. **Design System**: Does every UI mock / className snippet in the spec match the DS canon — semantic status tokens (no `text-red-*` / `bg-green-*`), Tailwind text scale (no `text-[11px]` / `text-[13px]`), shared primitives (`StatusBadge`, `Alert`, `FormField`, `SectionHeader`, `CollapsibleSection`, `LoadingMessage` / `Spinner` / `DataLoader`, `EmptyState`), lucide-react icons in page body (never inline `<svg>`), dialog `Cmd/Ctrl+Enter` submit + `Escape` cancel, `aria-label` on every icon-only button? See `AGENTS.md` → CRITICAL Rule #10 (Strict Design System alignment for every UI change) and `.ai/skills/backend-ui-design/SKILL.md`. Specs that touch existing pages MUST honour the Boy Scout rule.
63
66
 
64
67
  ## Quick Rule Reference
65
68
 
@@ -68,9 +71,15 @@ Use the [Specification Template](references/spec-template.md). Adapt if needed,
68
71
  - **`organization_id`** is mandatory for all tenant-scoped entities.
69
72
  - **Undoability** is the default for state changes.
70
73
  - **Zod validation** for all API inputs.
74
+ - **Encryption maps** for every sensitive / GDPR-relevant column (declare in `<module>/encryption.ts`, read via `findWithDecryption`) — see `AGENTS.md` → Data Encryption.
75
+ - **Canonical primitives** for CRUD APIs (`makeCrudRoute`), backend forms (`CrudForm`), tables (`DataTable`), HTTP (`apiCall` — never raw `fetch`), non-`CrudForm` writes (`useGuardedMutation`), cache (DI-resolved `@open-mercato/cache`), events (`createModuleEvents`) — see `AGENTS.md` → Mandatory Module Mechanisms.
76
+ - **Design System** tokens and shared UI primitives — no hardcoded status colors, no arbitrary text sizes, no inline `<svg>` in page-body UI. See `AGENTS.md` → Design System.
71
77
 
72
78
  ## Reference Materials
73
79
 
74
80
  - [Spec Template](references/spec-template.md)
75
- - [Spec Checklist](references/spec-checklist.md)
76
- - [AGENTS.md](../../../AGENTS.md)
81
+ - [Spec Checklist](references/spec-checklist.md) — § 3 covers encryption maps; § 5 covers canonical mechanisms + DS
82
+ - [AGENTS.md](../../../AGENTS.md) — Mandatory Module Mechanisms table; CRITICAL Rule #10 (Design System); CRITICAL Rule #11 (Encryption maps)
83
+ - [`.ai/skills/data-model-design/SKILL.md`](../data-model-design/SKILL.md) → Sensitive Data and Encryption Maps
84
+ - [`.ai/skills/module-scaffold/SKILL.md`](../module-scaffold/SKILL.md) → Encryption maps
85
+ - [`.ai/skills/backend-ui-design/SKILL.md`](../backend-ui-design/SKILL.md) — DS-compliant pages
@@ -19,26 +19,42 @@ Every item must be answered in the spec or marked N/A with justification.
19
19
 
20
20
  ## 3. Data Integrity & Security
21
21
 
22
- - [ ] Entities include `id`, `organization_id`, `created_at`, `updated_at`
22
+ - [ ] Entities include `id`, `organization_id`, `tenant_id`, `created_at`, `updated_at`, `deleted_at`, `is_active`
23
23
  - [ ] Write operations define transaction boundaries
24
24
  - [ ] Input validation uses zod schemas
25
25
  - [ ] All user input validated before business logic/persistence
26
- - [ ] Auth guards are declared (`requireAuth`, `requireRoles`, `requireFeatures`)
27
- - [ ] Tenant isolation: every scoped query filters by `organization_id`
26
+ - [ ] Auth guards declared per-method in `metadata` (`requireAuth`, `requireFeatures`) — never legacy top-level `export const requireAuth`
27
+ - [ ] Tenant isolation: every scoped query filters by `organization_id` (and `tenant_id` where applicable)
28
+ - [ ] **Encryption maps mechanism is used (no hand-rolled crypto).** For every PII / GDPR-relevant column the spec proposes — names, addresses, contacts, free-text notes about people, integration credentials, secrets, document numbers — the spec MUST declare them in a module-level `<module>/encryption.ts` exporting `defaultEncryptionMaps: ModuleEncryptionMap[]` (type from `@open-mercato/shared/modules/encryption`). Reads MUST go through `findWithDecryption` / `findOneWithDecryption` (5-arg `(em, entity, where, options?, scope?)`) from `@open-mercato/shared/lib/encryption/find`. Equality-lookup columns (e.g. login email) declare a sibling `hashField`. No `crypto.subtle`, no custom KMS calls, no "TODO encrypt later". See `AGENTS.md` → CRITICAL Rule #11 (Encryption maps) + the "Encryption maps for sensitive data" row of the Mandatory Module Mechanisms table; `.ai/skills/data-model-design/SKILL.md` § Sensitive Data and Encryption Maps.
28
29
 
29
30
  ## 4. Commands, Events & Naming
30
31
 
31
32
  - [ ] Naming is singular and consistent
32
33
  - [ ] All mutations are commands with undo logic
33
- - [ ] Events declared in `events.ts` before emitting
34
+ - [ ] Events declared in `<module>/events.ts` via `createModuleEvents({ moduleId, events } as const)` before emitting; cross-module side effects use `subscribers/*.ts`, never direct cross-module imports
34
35
  - [ ] Side-effect reversibility is documented
35
36
 
36
- ## 5. API & UI
37
+ ## 5. API & UI — Canonical Mechanisms
37
38
 
38
39
  - [ ] API contracts are complete (request/response/errors)
39
40
  - [ ] Routes include `openApi` expectations
40
- - [ ] UI uses `CrudForm`, `DataTable`, and shared primitives
41
- - [ ] i18n keys are planned for user-facing strings
41
+ - [ ] **Canonical mechanisms no DIY substitutes.** The spec MUST reach for the framework primitives, not invent its own. See `AGENTS.md` **Mandatory Module Mechanisms**.
42
+ - [ ] **CRUD APIs** use `makeCrudRoute({ entity, entityId, operations, schema, indexer: { entityType } })` from `@open-mercato/shared/lib/crud/factory`. Custom write routes call `validateCrudMutationGuard` before mutation and `runCrudMutationGuardAfterSuccess` after.
43
+ - [ ] **API route files export `metadata`** with per-method `requireAuth` / `requireFeatures` (no top-level `export const requireAuth`).
44
+ - [ ] **Backend forms** use `<CrudForm>` from `@open-mercato/ui/backend/CrudForm` with helpers `createCrud` / `updateCrud` / `deleteCrud` from `@open-mercato/ui/backend/utils/crud`, throwing `createCrudFormError` from `@open-mercato/ui/backend/utils/serverErrors` for field-level errors. No raw `<form>`, no raw `fetch`.
45
+ - [ ] **Lists** use `<DataTable entityId apiPath columns />` from `@open-mercato/ui/backend/DataTable` with stable `entityId` / `extensionTableId` so widget injection (columns / row actions / bulk actions / filters / toolbar) keeps working.
46
+ - [ ] **HTTP clients** use `apiCall` / `apiCallOrThrow` / `readApiResultOrThrow` from `@open-mercato/ui/backend/utils/apiCall` — never raw `fetch`.
47
+ - [ ] **Non-`CrudForm` writes** are wrapped in `useGuardedMutation(...).runMutation(...)` and pass `retryLastMutation` in the injection context.
48
+ - [ ] **Cache** is resolved via DI (`container.resolve('cache')`) — never `new Redis(...)` or raw SQLite. Tags include `tenant:<id>` / `org:<id>`.
49
+ - [ ] **Design System compliance for every UI mock and className snippet in the spec.** See `AGENTS.md` → CRITICAL Rule #10 (Strict Design System alignment) and `.ai/skills/backend-ui-design/SKILL.md`.
50
+ - [ ] Use semantic status tokens (`text-status-error-text`, `bg-status-success-bg`, `border-status-warning-border`, `text-status-info-icon`, `text-destructive`, `bg-destructive`) — NEVER hardcoded shades like `text-red-500`, `bg-green-100`, `text-amber-*`, `text-emerald-*`, `bg-blue-*`. Status tokens already cover dark mode; no `dark:` overrides.
51
+ - [ ] Use the Tailwind text scale (`text-xs` 12, `text-sm` 14, `text-base` 16, `text-lg` 18, `text-xl` 20, `text-2xl` 24) or `text-overline` for 11px uppercase labels — NEVER arbitrary sizes (`text-[11px]`, `text-[13px]`, `text-[15px]`, `p-[13px]`, `rounded-[24px]`, `z-[9999]`).
52
+ - [ ] Use shared primitives instead of raw HTML: `<Alert variant=...>` for inline status, `flash('msg', 'success|error|warning|info')` for toasts, `useConfirmDialog()` for destructive confirmations, `<StatusBadge>` for entity status, `<FormField label error>` to wrap form inputs, `<SectionHeader title count action>` for section headers, `<CollapsibleSection>` for collapsible regions, `<LoadingMessage>` / `<Spinner>` / `<DataLoader>` for async states, `<EmptyState>` (or DataTable `emptyState` prop) for empty lists.
53
+ - [ ] Use lucide-react icons in PAGE BODY UI (`Page`, `DataTable`, `CrudForm`, cards, buttons) — never inline `<svg>`. Sizes from the `size-{3|4|5|6}` scale; `strokeWidth` is not overridden per-instance. `page.meta.ts` icons follow the `React.createElement('svg', …)` pattern.
54
+ - [ ] Every dialog supports `Cmd/Ctrl+Enter` to submit and `Escape` to cancel.
55
+ - [ ] Every icon-only button has an `aria-label`.
56
+ - [ ] Boy Scout rule: when the spec edits an existing page, any line touched gets migrated to semantic tokens / DS scale.
57
+ - [ ] i18n keys are planned for user-facing strings (`useT()` client-side, `resolveTranslations()` server-side; never hard-coded labels)
42
58
  - [ ] Pagination limits defined (`pageSize <= 100`)
43
59
 
44
60
  ## 6. Risks & Anti-Patterns
@@ -48,3 +64,4 @@ Every item must be answered in the spec or marked N/A with justification.
48
64
  - [ ] Does not introduce cross-module ORM links
49
65
  - [ ] Does not skip undoability for state changes
50
66
  - [ ] Does not mix MVP with speculative future phases
67
+ - [ ] Does not introduce hand-rolled AES, raw `fetch`, raw `<form>`, `new Redis(...)`, or arbitrary Tailwind sizes / status colors
@@ -1,6 +1,6 @@
1
1
  # Agent Context Routing — {{PROJECT_NAME}}
2
2
 
3
- **MANDATORY CONTEXT LOADING** — see Critical Rule #5 below.
3
+ **MANDATORY CONTEXT LOADING** — see the "BEFORE writing ANY code" Critical Rule below.
4
4
  Before writing code, find your task below and `Read` the listed files.
5
5
  Do NOT load the entire src/ tree — Open Mercato apps can have many modules.
6
6
 
@@ -51,7 +51,8 @@ step, you WILL produce incorrect imports and miss required patterns.
51
51
  | Add caching | `.ai/guides/cache.md` |
52
52
  | Add background workers | `.ai/guides/queue.md` |
53
53
  | Use i18n (translations) | `.ai/guides/shared.md` → i18n |
54
- | Use encrypted queries | `.ai/guides/shared.md` → Encryption |
54
+ | Use encrypted queries (read sensitive columns that already have an encryption map; for authoring a NEW sensitive column see the row below first) | `.ai/guides/shared.md` → Encryption |
55
+ | **Encrypt sensitive / GDPR-relevant fields** ("we need this column encrypted", "store this securely", "this is PII", "GDPR", "encryption at rest", addresses, contact info, free-text notes about people, integration credentials, secrets) — declare them in the framework's encryption-maps mechanism, never hand-rolled AES/KMS | `.ai/skills/data-model-design/SKILL.md` → Sensitive Data and Encryption Maps, then `.ai/skills/module-scaffold/SKILL.md` → Encryption maps. Reference: <https://docs.open-mercato.dev/user-guide/encryption> |
55
56
  | Use apiCall / UI components | `.ai/guides/ui.md` |
56
57
  | Add permissions (RBAC) | `.ai/guides/core.md` → Access Control |
57
58
  | Add notifications | `.ai/guides/core.md` → Notifications |
@@ -130,11 +131,35 @@ src/modules/<id>/
130
131
  ├── ce.ts # Custom entities / custom field sets
131
132
  ├── translations.ts # Translatable fields per entity
132
133
  ├── notifications.ts # Notification type definitions
133
- └── notifications.client.ts # Client-side notification renderers
134
+ ├── notifications.client.ts # Client-side notification renderers
135
+ └── encryption.ts # Tenant data encryption maps (defaultEncryptionMaps) for sensitive / GDPR fields
134
136
  ```
135
137
 
136
138
  Register in `src/modules.ts`: `{ id: '<id>', from: '@app' }`
137
139
 
140
+ ## Mandatory Module Mechanisms (every module MUST use these — no DIY substitutes)
141
+
142
+ When the user asks to **create a new application** or a **new module**, do not invent your own routing, auth, persistence, forms, or caching. The framework provides one canonical primitive for each concern. If a feature is not on this list and not in the Task → Context Map, ask before adding it — do not roll your own.
143
+
144
+ | Concern | Canonical mechanism | Where to learn it |
145
+ |---|---|---|
146
+ | Module structure & auto-discovery | `src/modules/<id>/{api,backend,frontend,data,subscribers,workers,widgets}` + `index.ts` + `src/modules.ts` (`from: '@app'`) — discovered by `yarn generate` | `.ai/skills/module-scaffold/SKILL.md`, `.ai/guides/core.md` → Module Files; <https://docs.open-mercato.dev/framework/modules/overview> |
147
+ | **Backend admin pages** | Auto-discovered files under `src/modules/<id>/backend/**`, paired `page.meta.ts` with `requireAuth` + `requireFeatures` + `pageGroup`/`pageGroupKey`/`pageOrder` | `.ai/skills/backend-ui-design/SKILL.md`, `.ai/skills/module-scaffold/references/navigation-patterns.md`; <https://docs.open-mercato.dev/framework/modules/routes-and-pages> |
148
+ | **Frontend public pages** | Auto-discovered files under `src/modules/<id>/frontend/**`. Customer portal pages live under `frontend/[orgSlug]/portal/<path>/page.tsx` with `requireCustomerAuth` / `requireCustomerFeatures` in `page.meta.ts` | `.ai/guides/ui.md` → Portal Extension; <https://docs.open-mercato.dev/framework/modules/routes-and-pages> |
149
+ | **API routes** | Files under `src/modules/<id>/api/**/route.ts` exporting handlers + `metadata` (per-method `requireAuth` / `requireFeatures`) + `openApi`. NEVER write a top-level `export const requireAuth` — the registry no longer recognises it | `.ai/guides/core.md` → API Routes; <https://docs.open-mercato.dev/framework/api/api-development-guide> |
150
+ | **CRUD APIs (factory)** | `makeCrudRoute({ entity, entityId, operations, schema, indexer: { entityType } })` from `@open-mercato/shared/lib/crud/factory`. Always set `indexer` so query-index coverage stays correct. Custom (non-`makeCrudRoute`) write routes MUST call `validateCrudMutationGuard` before the mutation and `runCrudMutationGuardAfterSuccess` after success | `.ai/skills/module-scaffold/SKILL.md` → Create API Routes; <https://docs.open-mercato.dev/framework/api/crud-factory> |
151
+ | **CRUD forms in admin** | `<CrudForm entityId apiPath mode fields />` from `@open-mercato/ui/backend/CrudForm`; helpers `createCrud` / `updateCrud` / `deleteCrud` from `@open-mercato/ui/backend/utils/crud`; `createCrudFormError` from `@open-mercato/ui/backend/utils/serverErrors`. Never raw `<form>`, never raw `fetch` | `.ai/skills/backend-ui-design/SKILL.md`; <https://docs.open-mercato.dev/framework/admin-ui/crud-form> |
152
+ | **DataTables in admin** | `<DataTable entityId apiPath title columns />` from `@open-mercato/ui/backend/DataTable`. Keep `entityId` and `extensionTableId` stable so widget injection (columns, row actions, bulk actions, filters, toolbar) keeps working | `.ai/skills/backend-ui-design/SKILL.md`; <https://docs.open-mercato.dev/framework/admin-ui/data-grids> |
153
+ | **Authorization (RBAC)** | Declare features in `<module>/acl.ts`, grant them in `<module>/setup.ts` `defaultRoleFeatures`, gate pages and routes with `requireFeatures` in `metadata` / `page.meta.ts`. NEVER use `requireRoles` (role names mutate). Run `yarn mercato auth sync-role-acls` after adding new features | `.ai/guides/core.md` → Access Control; <https://docs.open-mercato.dev/framework/rbac/overview> |
154
+ | **Multi-tenant scoping (default for every entity)** | Every tenant-scoped entity MUST include indexed `organization_id` and `tenant_id` columns and every read/write MUST filter by them. The CRUD factory injects scope automatically — do NOT bypass it. For ad-hoc queries use `withScopedPayload` from `@open-mercato/shared/lib/api/scoped` | `.ai/skills/data-model-design/SKILL.md`; <https://docs.open-mercato.dev/architecture/system-overview> |
155
+ | **Encryption maps for sensitive data** | Declare a module-level `<module>/encryption.ts` exporting `defaultEncryptionMaps: ModuleEncryptionMap[]` from `@open-mercato/shared/modules/encryption`. Read encrypted columns via `findWithDecryption` / `findOneWithDecryption` from `@open-mercato/shared/lib/encryption/find`. NEVER hand-roll AES/KMS, NEVER use `em.find` on encrypted columns | `.ai/skills/data-model-design/SKILL.md` → Sensitive Data and Encryption Maps; <https://docs.open-mercato.dev/user-guide/encryption> |
156
+ | **Cache** | Resolve the cache from DI (`container.resolve('cache')`) — never `new Redis(...)` or raw SQLite. Tag with `tenant:<id>` / `org:<id>` and the entity-type tag so invalidation stays tenant-scoped | `.ai/guides/cache.md`; <https://docs.open-mercato.dev/user-guide/cache-management> |
157
+ | **Background workers** | `src/modules/<id>/workers/*.ts` exporting `metadata: { queue, id?, concurrency? }` + default handler. Never spin up custom queues | `.ai/guides/queue.md`; <https://docs.open-mercato.dev/framework/events/queue-workers> |
158
+ | **Events between modules** | `<module>/events.ts` with `createModuleEvents({ moduleId, events } as const)`. Subscribers in `subscribers/*.ts`. Never call other modules' services directly across module boundaries | `.ai/guides/events.md`; <https://docs.open-mercato.dev/framework/events/overview> |
159
+ | **i18n (every user-facing string)** | `useT()` client-side from `@open-mercato/shared/lib/i18n/context`, `resolveTranslations()` server-side from `@open-mercato/shared/lib/i18n/server`; keys in `src/i18n/<locale>.json`. Never hard-code labels in components | `.ai/guides/shared.md` → i18n |
160
+
161
+ > Rule of thumb: if you find yourself reaching for raw `fetch`, raw `<form>`, ad-hoc `crypto`, ad-hoc `Redis`, or a manual `m2m` join across modules, stop and check the row above — there is a canonical helper.
162
+
138
163
  ## CRITICAL rules — always follow without exception
139
164
 
140
165
  1. **Entity classes live in `src/modules/<module>/data/entities.ts` and MUST import decorators from `@mikro-orm/decorators/legacy`.** Start there for every schema change.
@@ -156,9 +181,16 @@ Register in `src/modules.ts`: `{ id: '<id>', from: '@app' }`
156
181
  **Dashboards fallback rule.** When the user (or the `trim-unused-modules` skill) disables the `dashboards` module, you MUST update `src/app/(backend)/backend/page.tsx` so it no longer renders `<DashboardScreen />`. Replace the dashboard render with a `redirect(...)` to the first enabled backend page for the current user — preferring pages already registered in the main sidebar group and respecting the admin/superadmin role of the caller. Otherwise `/backend` will crash at build or request time because the removed module no longer ships `DashboardScreen`. Always fall back to `/backend/profile` only if no other backend page is available.
157
182
  9. **New features MUST be visible to default roles immediately.** Every time you add a new feature ID (e.g. `my_module.view`, `my_module.manage`) to `src/modules/<module>/acl.ts`, you MUST also (a) add that feature to `defaultRoleFeatures` in the same module's `setup.ts` so the admin role and any other appropriate default roles get it on every tenant setup; and (b) run `yarn mercato auth sync-role-acls` so existing tenants pick up the new feature without a reinstall. Use `--tenant <tenantId>` only when the user asks to target one tenant. Do this automatically unless the user has explicitly said otherwise — the user should see the features you are building, not stare at a blank admin because their role is missing a grant. Feature IDs are FROZEN once shipped; if a rename is required, add the new ID alongside, grant it, and keep the old one as a deprecated alias.
158
183
  10. **Strict Design System alignment for every UI change.** Any UI you add or edit MUST use the Open Mercato design system components and tokens. No hardcoded Tailwind status colors (`text-red-500`, `bg-green-100`, etc.) — use semantic tokens (`text-status-error-text`, `bg-status-success-bg`, …). No arbitrary text sizes (`text-[11px]`, `text-[13px]`) — use the Tailwind scale (`text-xs`, `text-sm`, `text-base`, `text-lg`, `text-xl`, `text-2xl`) or the `text-overline` token for 11px uppercase labels. In PAGE BODY UI, use `lucide-react` icons (never inline `<svg>`). Use `StatusBadge` for entity status, `Alert` for inline feedback, `FormField` for standalone form inputs, `SectionHeader` for detail-page section headings, `CollapsibleSection` for collapsible regions, `LoadingMessage`/`Spinner`/`DataLoader` for async states, and `EmptyState` (or DataTable's `emptyState` prop) for empty lists. For list pages, follow `.ai/skills/backend-ui-design/SKILL.md` and prefer the `DataTable` host pattern shown there (`entityId`, `apiPath`, stable `extensionTableId`, and explicit pagination props when you own the data source). Every dialog MUST support `Cmd/Ctrl+Enter` to submit and `Escape` to cancel. Every icon-only button MUST have an `aria-label`. These rules apply to `src/modules/<module>/backend/**` and `src/modules/<module>/frontend/**` alike.
159
- 11. **BEFORE writing ANY code**, you MUST:
184
+ 11. **Sensitive / GDPR fields MUST go through the encryption-maps mechanism — never hand-rolled.** The framework provides per-tenant DEKs, KMS-backed key resolution, and a declarative field-level map. Whenever the user asks for "this field encrypted", "store this securely", "this is PII", "GDPR", "encryption at rest", or you are designing a column that will hold names, addresses, contacts, free-text notes about people, integration secrets/credentials, or any data subject to a data-processing agreement, you MUST:
185
+ - Declare the entity + field list in `src/modules/<module>/encryption.ts` exporting `defaultEncryptionMaps: ModuleEncryptionMap[]` (type imported from `@open-mercato/shared/modules/encryption`).
186
+ - Read those columns via `findWithDecryption` / `findOneWithDecryption` from `@open-mercato/shared/lib/encryption/find` (passing `tenantId` and `organizationId`). Never use raw `em.find` on encrypted columns.
187
+ - For deterministic-lookup fields (e.g., login email), declare a sibling `hashField` in the map so equality lookups still work.
188
+ - Run `yarn mercato entities seed-encryption --tenant <tenantId>` after adding maps so existing tenants pick them up; new tenants get them automatically during `auth:setup`.
189
+ - Treat hand-rolled AES, raw `crypto.subtle`, custom KMS calls, or storing plaintext "for now" as broken — rewrite via the maps. See `.ai/skills/data-model-design/SKILL.md` → Sensitive Data and Encryption Maps and <https://docs.open-mercato.dev/user-guide/encryption>.
190
+ 12. **BEFORE writing ANY code**, you MUST:
160
191
  - Match your task against the **Task → Context Map** above
161
192
  - `Read` every file listed in the "Load" column for your task type
193
+ - Read the **Mandatory Module Mechanisms** section above to confirm which canonical primitives apply (CRUD factory, CrudForm, DataTable, RBAC, multi-tenant scoping, encryption maps, cache, events) — do not invent your own substitutes
162
194
  - Only then proceed to implementation
163
195
  - If your task matches multiple rows, load ALL listed files
164
196
  - **Do NOT skip this step.** The guides contain canonical import paths, required patterns, and conventions that CANNOT be reliably inferred from existing code alone. Skipping leads to wrong imports, missing conventions, and rework.
@@ -197,7 +229,8 @@ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
197
229
  import { apiCall, apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
198
230
 
199
231
  // CRUD forms
200
- import { CrudForm, createCrud, updateCrud, deleteCrud } from '@open-mercato/ui/backend/crud'
232
+ import { CrudForm, type CrudField, type CrudFormGroup } from '@open-mercato/ui/backend/CrudForm'
233
+ import { createCrud, updateCrud, deleteCrud } from '@open-mercato/ui/backend/utils/crud'
201
234
  import { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'
202
235
 
203
236
  // UI components (MUST use — never raw <button>)
@@ -1,9 +1,14 @@
1
1
  import { defineConfig } from '@playwright/test'
2
2
  import path from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
3
4
  import { discoverIntegrationSpecFiles } from '@open-mercato/cli/lib/testing/integration-discovery'
4
5
 
5
6
  const captureScreenshots = process.env.PW_CAPTURE_SCREENSHOTS === '1'
6
7
  const isGitHubActions = process.env.GITHUB_ACTIONS === 'true'
8
+ // Standalone apps generated from this template declare `"type": "module"`,
9
+ // so the CommonJS `__dirname` is undefined when Playwright loads this config
10
+ // under the Node ESM loader. Reconstruct it from `import.meta.url`.
11
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
7
12
  const projectRoot = path.resolve(__dirname, '..', '..', '..')
8
13
  const qaTestResultsRoot = path.join(projectRoot, '.ai', 'qa', 'test-results')
9
14
  const normalizePath = (value: string) => value.split(path.sep).join('/')
@@ -16,7 +16,8 @@ Design entities, relationships, and manage the migration lifecycle following Ope
16
16
  5. [Cross-Module References](#5-cross-module-references)
17
17
  6. [Migration Lifecycle](#6-migration-lifecycle)
18
18
  7. [Advanced Patterns](#7-advanced-patterns)
19
- 8. [Anti-Patterns](#8-anti-patterns)
19
+ 8. [Sensitive Data and Encryption Maps](#8-sensitive-data-and-encryption-maps)
20
+ 9. [Anti-Patterns](#9-anti-patterns)
20
21
 
21
22
  ---
22
23
 
@@ -483,7 +484,89 @@ export class TicketHistory {
483
484
 
484
485
  ---
485
486
 
486
- ## 8. Anti-Patterns
487
+ ## 8. Sensitive Data and Encryption Maps
488
+
489
+ When the developer asks for "we need this column encrypted", "store this securely", "this is PII", "GDPR", or "encryption at rest" — and whenever you are designing a column that will hold names, addresses, contact information, free-text notes about people, integration credentials, secrets, or any data subject to a data-processing agreement — use the framework's **encryption-maps mechanism**. Do NOT hand-roll AES, raw `crypto.subtle`, custom KMS calls, or "TODO encrypt later" stubs.
490
+
491
+ The mechanism gives you:
492
+
493
+ - Per-tenant Data Encryption Keys (DEKs) resolved through the configured KMS (Vault by default, env-fallback in dev).
494
+ - Declarative, per-entity, per-field encryption with optional deterministic-hash sibling columns for equality lookups (for example login by email).
495
+ - Boot-time auto-application: every enabled module's `defaultEncryptionMaps` is collected during `auth:setup` and applied when `TENANT_DATA_ENCRYPTION=yes`.
496
+ - A `findWithDecryption` / `findOneWithDecryption` read API that transparently decrypts on read.
497
+
498
+ ### When encryption is mandatory
499
+
500
+ | Field example | Encrypt? |
501
+ |---|---|
502
+ | First name, last name, preferred name | Yes |
503
+ | Email, phone | Yes — usually with a `hashField` for lookups |
504
+ | Postal address (line 1/2, city, region, postal code, country) | Yes |
505
+ | Free-text comments / notes / activity bodies that mention people | Yes |
506
+ | Integration secrets, API keys, OAuth tokens, webhook signing keys | Yes |
507
+ | Document numbers (tax IDs, national IDs) | Yes |
508
+ | Status enums, counters, timestamps, FKs, currency codes | No |
509
+ | Public catalog metadata (product titles for a public storefront) | Usually no |
510
+
511
+ If you are unsure, default to encrypting and confirm with the user — re-introducing encryption later requires a backfill, but turning it off later is a single map edit.
512
+
513
+ ### Declare the map in `<module>/encryption.ts`
514
+
515
+ ```typescript
516
+ import type { ModuleEncryptionMap } from '@open-mercato/shared/modules/encryption'
517
+
518
+ export const defaultEncryptionMaps: ModuleEncryptionMap[] = [
519
+ {
520
+ entityId: '<module_id>:<entity>', // matches the entity's table id (colon-separated)
521
+ fields: [
522
+ { field: 'first_name' },
523
+ { field: 'last_name' },
524
+ { field: 'phone' },
525
+ // Sibling deterministic hash for equality lookups (e.g. login by email).
526
+ // Add a matching `<field>_hash varchar` column to the entity.
527
+ { field: 'email', hashField: 'email_hash' },
528
+ ],
529
+ },
530
+ ]
531
+
532
+ export default defaultEncryptionMaps
533
+ ```
534
+
535
+ ### Read with decryption — never raw `em.find`
536
+
537
+ ```typescript
538
+ import { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
539
+
540
+ // Signature: (em, entityName, where, options?, scope?). MikroORM FindOptions go in slot 4
541
+ // (pass `undefined` if you have none), the decryption scope `{ tenantId, organizationId }` in slot 5.
542
+ const records = await findWithDecryption(em, '<Entity>', filter, undefined, { tenantId, organizationId })
543
+ const single = await findOneWithDecryption(em, '<Entity>', { id }, undefined, { tenantId, organizationId })
544
+ ```
545
+
546
+ Calling `em.find` on an encrypted column returns ciphertext, breaks search, and silently leaks bug surface. The `findWithDecryption` family is the one entry point.
547
+
548
+ ### Apply maps to existing tenants
549
+
550
+ ```bash
551
+ yarn mercato entities seed-encryption --tenant <tenantId> [--organization <orgId>]
552
+ ```
553
+
554
+ New tenants pick up the maps automatically during `auth:setup`. Toggling the **Encrypted** flag on a custom field via the admin UI also only applies to data written **after** the change — backfill historical plaintext rows by running `yarn mercato entities rotate-encryption-key --tenant <tenantId> --org <organizationId>` (without `--old-key` it skips already-encrypted fields and just encrypts plaintext). Use `yarn mercato entities decrypt-database` to roll back. For full UI flows and CLI options see <https://docs.open-mercato.dev/user-guide/encryption>.
555
+
556
+ ### Vector search caveat
557
+
558
+ The `vector` module stores raw embeddings unencrypted in the vector store (e.g. pgvector). Even though the source text is decrypted only transiently to compute embeddings, treat the embeddings as sensitive: avoid embedding raw high-sensitivity text and rely on disk-level / managed-database encryption-at-rest for the vector column.
559
+
560
+ ### Environment switches
561
+
562
+ - `TENANT_DATA_ENCRYPTION=yes|no` (default `yes`) — set to `no` to run the hooks as no-op (validation still applies).
563
+ - `TENANT_DATA_ENCRYPTION_DEBUG=yes` — log map evaluation, KMS calls, cache hits.
564
+ - `VAULT_ADDR` / `VAULT_TOKEN` / `VAULT_KV_PATH` — HashiCorp Vault KMS configuration.
565
+ - `TENANT_DATA_ENCRYPTION_FALLBACK_KEY` — local/dev fallback key when Vault is unavailable. In dev, `AUTH_SECRET` / `NEXTAUTH_SECRET` is used as a last resort; production falls back to noop KMS.
566
+
567
+ ---
568
+
569
+ ## 9. Anti-Patterns
487
570
 
488
571
  | Anti-Pattern | Problem | Correct Pattern |
489
572
  |-------------|---------|-----------------|
@@ -498,6 +581,10 @@ export class TicketHistory {
498
581
  | Storing arrays as comma-separated strings | Can't query, no integrity | Use `jsonb` arrays or junction tables |
499
582
  | UUID FK without index | Slow joins | Always `@Index()` on FK columns |
500
583
  | Nullable required fields | Data integrity issues | Use `!` assertion for required, `null` for optional |
584
+ | Hand-rolled AES / `crypto.subtle` / custom KMS for sensitive columns | Per-tenant key isolation, hash lookups, key rotation, and admin UI all break | Declare `<module>/encryption.ts` with `defaultEncryptionMaps`; let the framework manage DEKs and Vault |
585
+ | Reading encrypted columns with raw `em.find` / `em.findOne` | Returns ciphertext, breaks search, silent data corruption | Use `findWithDecryption` / `findOneWithDecryption` with `{ tenantId, organizationId }` |
586
+ | Storing PII as plaintext "for now" / TODO comments | GDPR violation, leaks at rest, expensive backfill later | Encrypt from day one; toggling later only protects new writes |
587
+ | Encrypting an `email` column without a `hashField` | Login / equality lookups stop working | Declare a sibling `hashField` (e.g. `email_hash`) in the encryption map and add the matching `varchar` column |
501
588
 
502
589
  ---
503
590
 
@@ -515,7 +602,8 @@ export class TicketHistory {
515
602
  - **MUST** specify `length` on all `varchar` columns
516
603
  - **MUST NOT** use ORM relationship decorators across module boundaries
517
604
  - **MUST NOT** rename or drop columns in a single release
518
- - **MUST NOT** store sensitive data without encryption (use `findWithDecryption`)
605
+ - **MUST** declare encrypted columns in `<module>/encryption.ts` exporting `defaultEncryptionMaps: ModuleEncryptionMap[]`, and read them via `findWithDecryption` / `findOneWithDecryption` from `@open-mercato/shared/lib/encryption/find` — see section 8
606
+ - **MUST NOT** hand-roll AES / KMS calls or store sensitive columns as plaintext "for now" — use the encryption-maps mechanism in section 8
519
607
  - Use `jsonb` for flexible/nested data, proper columns for queryable/sortable data
520
608
  - Use junction tables for many-to-many relationships
521
609
  - Derive TypeScript types from Zod schemas, never duplicate type definitions
@@ -40,14 +40,18 @@ For every piece of code, enforce these code-review rules inline:
40
40
  | Area | Rule |
41
41
  |------|------|
42
42
  | Types | No `any` — use zod + `z.infer` |
43
- | API routes | Export `openApi` and `metadata` with auth guards |
44
- | Entities | Standard columns, snake_case, UUID PKs, `organization_id` + `tenant_id` |
43
+ | API routes | Export `openApi` and per-method `metadata` with `requireAuth` / `requireFeatures` (no top-level `export const requireAuth`) |
44
+ | **CRUD APIs** | **Use `makeCrudRoute({ entity, entityId, operations, schema, indexer: { entityType } })` from `@open-mercato/shared/lib/crud/factory`. Custom write routes MUST call `validateCrudMutationGuard` before the mutation and `runCrudMutationGuardAfterSuccess` after success. See `AGENTS.md` → Mandatory Module Mechanisms.** |
45
+ | Entities | Standard columns, snake_case, UUID PKs, indexed `organization_id` + `tenant_id` |
45
46
  | Security | `findWithDecryption`, tenant scoping, zod validation |
46
- | UI | `CrudForm`/`DataTable`, `apiCall`, `flash()`, `LoadingMessage`/`ErrorMessage` |
47
- | Events | `createModuleEvents()` with `as const`, subscribers export `metadata` |
47
+ | **Encryption maps** | **For every PII / GDPR-relevant column the phase touches, declare in `<module>/encryption.ts` exporting `defaultEncryptionMaps` (type from `@open-mercato/shared/modules/encryption`). Reads via `findWithDecryption` / `findOneWithDecryption` (5-arg `(em, entity, where, options?, scope?)`). Equality-lookup columns declare a sibling `hashField`. NEVER hand-rolled AES/KMS, `crypto.subtle`, or "encrypt later" stubs. See `AGENTS.md` → CRITICAL Rule #11 (Encryption maps) + the "Encryption maps" row of the Mandatory Module Mechanisms table; `.ai/skills/data-model-design/SKILL.md` § Sensitive Data and Encryption Maps; `.ai/skills/module-scaffold/SKILL.md` § Encryption maps.** |
48
+ | UI | `<CrudForm>`/`<DataTable>` (with stable `entityId` + `extensionTableId`), `apiCall` (never raw `fetch`), `flash()`, `<LoadingMessage>`/`<ErrorMessage>` |
49
+ | **Design System** | **Semantic status tokens (no `text-red-*` / `bg-green-*`); Tailwind text scale (no `text-[13px]` / `text-[11px]`); shared primitives `StatusBadge` / `Alert` / `FormField` / `SectionHeader` / `CollapsibleSection` / `LoadingMessage` / `Spinner` / `DataLoader` / `EmptyState`; lucide-react icons in PAGE BODY (never inline `<svg>`); `aria-label` on every icon-only button; Boy Scout rule on touched lines. See `AGENTS.md` → CRITICAL Rule #10 (Strict Design System alignment) + `.ai/skills/backend-ui-design/SKILL.md`.** |
50
+ | **Cache** | **Resolve via DI (`container.resolve('cache')`); tag with `tenant:<id>` / `org:<id>`; declare invalidation per write path. NEVER `new Redis(...)` or raw SQLite.** |
51
+ | Events | `createModuleEvents()` with `as const`, subscribers export `metadata`; cross-module side effects via subscribers, never direct imports |
48
52
  | i18n | `useT()` client, `resolveTranslations()` server, no hardcoded strings |
49
53
  | Imports | Package-level `@open-mercato/<pkg>/...` for framework imports |
50
- | Mutations | `useGuardedMutation` when not using CrudForm |
54
+ | Mutations | `useGuardedMutation` when not using CrudForm; pass `retryLastMutation` in injection context |
51
55
  | Keyboard | `Cmd/Ctrl+Enter` submit, `Escape` cancel on dialogs |
52
56
  | Naming | Modules plural snake_case, events `module.entity.past_tense`, features `module.action` |
53
57
 
@@ -41,8 +41,9 @@ Before writing any code, ask the developer:
41
41
  - [ ] Background workers
42
42
  - [ ] CLI commands
43
43
  - [ ] Custom fields support
44
+ - [ ] **Sensitive / GDPR-relevant fields** (PII, contact info, addresses, free-text notes about people, integration credentials, secrets) — if yes, an `encryption.ts` declaring `defaultEncryptionMaps` is mandatory; see section 11 → Encryption maps
44
45
 
45
- If the developer provides a brief description, infer reasonable defaults and confirm.
46
+ If the developer provides a brief description, infer reasonable defaults and confirm. When key fields include names, emails, phones, addresses, free-text comments, or external API keys, treat the encryption checkbox as `yes` by default and confirm with the user rather than skipping it silently.
46
47
 
47
48
  ---
48
49
 
@@ -57,6 +58,7 @@ src/modules/<module_id>/
57
58
  ├── setup.ts # Tenant init, role features
58
59
  ├── di.ts # Awilix DI registrations
59
60
  ├── events.ts # Typed event declarations (if needed)
61
+ ├── encryption.ts # Tenant data encryption maps (only if entity has sensitive/GDPR fields)
60
62
  ├── data/
61
63
  │ ├── entities.ts # MikroORM entity classes
62
64
  │ └── validators.ts # Zod validation schemas
@@ -178,7 +180,7 @@ Use `makeCrudRoute` for standard CRUD. Each HTTP method lives in its own file.
178
180
  **File**: `src/modules/<module_id>/api/get/<entities>.ts`
179
181
 
180
182
  ```typescript
181
- import { makeCrudRoute } from '@open-mercato/shared/lib/crud/make-crud-route'
183
+ import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
182
184
  import { <Entity> } from '../../data/entities'
183
185
 
184
186
  const handler = makeCrudRoute({
@@ -201,7 +203,7 @@ export const openApi = {
201
203
  **File**: `src/modules/<module_id>/api/post/<entities>.ts`
202
204
 
203
205
  ```typescript
204
- import { makeCrudRoute } from '@open-mercato/shared/lib/crud/make-crud-route'
206
+ import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
205
207
  import { <Entity> } from '../../data/entities'
206
208
  import { create<Entity>Schema } from '../../data/validators'
207
209
 
@@ -225,7 +227,7 @@ export const openApi = {
225
227
  **File**: `src/modules/<module_id>/api/put/<entities>.ts`
226
228
 
227
229
  ```typescript
228
- import { makeCrudRoute } from '@open-mercato/shared/lib/crud/make-crud-route'
230
+ import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
229
231
  import { <Entity> } from '../../data/entities'
230
232
  import { update<Entity>Schema } from '../../data/validators'
231
233
 
@@ -249,7 +251,7 @@ export const openApi = {
249
251
  **File**: `src/modules/<module_id>/api/delete/<entities>.ts`
250
252
 
251
253
  ```typescript
252
- import { makeCrudRoute } from '@open-mercato/shared/lib/crud/make-crud-route'
254
+ import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
253
255
  import { <Entity> } from '../../data/entities'
254
256
 
255
257
  const handler = makeCrudRoute({
@@ -346,7 +348,7 @@ export const metadata = {
346
348
 
347
349
  ```tsx
348
350
  'use client'
349
- import { CrudForm } from '@open-mercato/ui/backend/crud'
351
+ import { CrudForm } from '@open-mercato/ui/backend/CrudForm'
350
352
  import { useT } from '@open-mercato/shared/lib/i18n/context'
351
353
 
352
354
  export default function Create<Entity>Page() {
@@ -384,7 +386,7 @@ export const metadata = {
384
386
 
385
387
  ```tsx
386
388
  'use client'
387
- import { CrudForm } from '@open-mercato/ui/backend/crud'
389
+ import { CrudForm } from '@open-mercato/ui/backend/CrudForm'
388
390
  import { useT } from '@open-mercato/shared/lib/i18n/context'
389
391
 
390
392
  export default function Edit<Entity>Page({ params }: { params: { id: string } }) {
@@ -573,6 +575,52 @@ export default function registerCli(program: any) {
573
575
  }
574
576
  ```
575
577
 
578
+ ### Encryption maps (sensitive / GDPR-relevant fields)
579
+
580
+ **Mandatory** when the entity stores PII, contact info, addresses, free-text notes about people, integration credentials, secrets, or anything subject to a data-processing agreement. Do NOT hand-roll AES, KMS calls, or "TODO encrypt later" stubs — the framework provides per-tenant DEKs and a declarative field-level map.
581
+
582
+ **File**: `src/modules/<module_id>/encryption.ts`
583
+
584
+ ```typescript
585
+ import type { ModuleEncryptionMap } from '@open-mercato/shared/modules/encryption'
586
+
587
+ export const defaultEncryptionMaps: ModuleEncryptionMap[] = [
588
+ {
589
+ entityId: '<module_id>:<entity>', // matches data/entities.ts table id, colon-separated
590
+ fields: [
591
+ { field: 'first_name' },
592
+ { field: 'last_name' },
593
+ { field: 'phone' },
594
+ // Add a hashField for deterministic equality lookups (e.g. login by email):
595
+ { field: 'email', hashField: 'email_hash' },
596
+ ],
597
+ },
598
+ ]
599
+
600
+ export default defaultEncryptionMaps
601
+ ```
602
+
603
+ **Read paths** — never `em.find` an encrypted column directly:
604
+
605
+ ```typescript
606
+ import { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
607
+
608
+ // Signature: (em, entityName, where, options?, scope?) — MikroORM FindOptions in slot 4
609
+ // (pass `undefined` when none), decryption scope in slot 5.
610
+ const records = await findWithDecryption(em, '<Entity>', filter, undefined, { tenantId, organizationId })
611
+ const single = await findOneWithDecryption(em, '<Entity>', { id }, undefined, { tenantId, organizationId })
612
+ ```
613
+
614
+ **Apply to existing tenants** after declaring or updating maps:
615
+
616
+ ```bash
617
+ yarn mercato entities seed-encryption --tenant <tenantId> [--organization <orgId>]
618
+ ```
619
+
620
+ New tenants pick up `defaultEncryptionMaps` automatically during `auth:setup`. Toggling the **Encrypted** flag for a field only applies to data written **after** the change — historical plaintext rows stay as they were until backfilled via `yarn mercato entities rotate-encryption-key --tenant <tenantId> --org <organizationId>` (without `--old-key` the command only encrypts plaintext and skips already-encrypted fields). Use `yarn mercato entities decrypt-database` to roll back. For end-to-end usage and admin UI flows see <https://docs.open-mercato.dev/user-guide/encryption>.
621
+
622
+ > Tip: when `email` (or any other column) needs deterministic lookups while encrypted, declare a sibling `hashField` in the map and add a matching `varchar` column to the entity. The framework keeps the hash in sync on writes; queries can target the hash instead of the cleartext column.
623
+
576
624
  ---
577
625
 
578
626
  ## 12. Wire & Verify
@@ -658,3 +706,5 @@ yarn dev # Start dev server
658
706
  - **MUST NOT** run `yarn db:migrate` without explicit user confirmation
659
707
  - **MUST NOT** create ORM relationships (`@ManyToOne`, `@OneToMany`) to entities in other modules
660
708
  - **MUST NOT** edit `.mercato/generated/*` files manually
709
+ - **MUST** declare `<module>/encryption.ts` exporting `defaultEncryptionMaps` whenever the entity stores sensitive / GDPR-relevant fields (PII, contact info, addresses, free-text notes about people, integration credentials, secrets) — and read those columns via `findWithDecryption` / `findOneWithDecryption`
710
+ - **MUST NOT** hand-roll AES/KMS calls or store "we'll encrypt this later" plaintext for sensitive columns — use the encryption-maps mechanism described in section 11 → Encryption maps
@@ -60,6 +60,9 @@ Use the [Specification Template](references/spec-template.md). Adapt if needed,
60
60
  3. **Singularity Law**: Singular naming for entities, commands, events, feature IDs.
61
61
  4. **Undo Contract**: Is the "Undo" logic as detailed as the "Execute"?
62
62
  5. **Module Isolation**: Using Event Bus for side effects or cheating with direct imports?
63
+ 6. **Canonical Mechanisms**: Does the spec reach for the framework primitives (`makeCrudRoute`, `<CrudForm>`, `<DataTable>`, `apiCall` / `useGuardedMutation`, DI-resolved cache, `createModuleEvents`) or invent its own substitute? See `AGENTS.md` → **Mandatory Module Mechanisms** for the full canon and links. No raw `fetch`, no raw `<form>`, no `new Redis(...)`, no manual cross-module ORM joins.
64
+ 7. **Sensitive Data**: For every PII / GDPR / address / contact / free-text-about-people / integration-credential column the spec proposes, does it declare an `encryption.ts` `defaultEncryptionMaps` entry and route reads through `findWithDecryption`? See `AGENTS.md` → CRITICAL Rule #11 (Encryption maps) + the "Encryption maps for sensitive data" row of the Mandatory Module Mechanisms table and `.ai/skills/data-model-design/SKILL.md` § Sensitive Data and Encryption Maps. No hand-rolled AES, no `crypto.subtle`, no "TODO encrypt later".
65
+ 8. **Design System**: Does every UI mock / className snippet in the spec match the DS canon — semantic status tokens (no `text-red-*` / `bg-green-*`), Tailwind text scale (no `text-[11px]` / `text-[13px]`), shared primitives (`StatusBadge`, `Alert`, `FormField`, `SectionHeader`, `CollapsibleSection`, `LoadingMessage` / `Spinner` / `DataLoader`, `EmptyState`), lucide-react icons in page body (never inline `<svg>`), dialog `Cmd/Ctrl+Enter` submit + `Escape` cancel, `aria-label` on every icon-only button? See `AGENTS.md` → CRITICAL Rule #10 (Strict Design System alignment for every UI change) and `.ai/skills/backend-ui-design/SKILL.md`. Specs that touch existing pages MUST honour the Boy Scout rule.
63
66
 
64
67
  ## Quick Rule Reference
65
68
 
@@ -68,9 +71,15 @@ Use the [Specification Template](references/spec-template.md). Adapt if needed,
68
71
  - **`organization_id`** is mandatory for all tenant-scoped entities.
69
72
  - **Undoability** is the default for state changes.
70
73
  - **Zod validation** for all API inputs.
74
+ - **Encryption maps** for every sensitive / GDPR-relevant column (declare in `<module>/encryption.ts`, read via `findWithDecryption`) — see `AGENTS.md` → Data Encryption.
75
+ - **Canonical primitives** for CRUD APIs (`makeCrudRoute`), backend forms (`CrudForm`), tables (`DataTable`), HTTP (`apiCall` — never raw `fetch`), non-`CrudForm` writes (`useGuardedMutation`), cache (DI-resolved `@open-mercato/cache`), events (`createModuleEvents`) — see `AGENTS.md` → Mandatory Module Mechanisms.
76
+ - **Design System** tokens and shared UI primitives — no hardcoded status colors, no arbitrary text sizes, no inline `<svg>` in page-body UI. See `AGENTS.md` → Design System.
71
77
 
72
78
  ## Reference Materials
73
79
 
74
80
  - [Spec Template](references/spec-template.md)
75
- - [Spec Checklist](references/spec-checklist.md)
76
- - [AGENTS.md](../../../AGENTS.md)
81
+ - [Spec Checklist](references/spec-checklist.md) — § 3 covers encryption maps; § 5 covers canonical mechanisms + DS
82
+ - [AGENTS.md](../../../AGENTS.md) — Mandatory Module Mechanisms table; CRITICAL Rule #10 (Design System); CRITICAL Rule #11 (Encryption maps)
83
+ - [`.ai/skills/data-model-design/SKILL.md`](../data-model-design/SKILL.md) → Sensitive Data and Encryption Maps
84
+ - [`.ai/skills/module-scaffold/SKILL.md`](../module-scaffold/SKILL.md) → Encryption maps
85
+ - [`.ai/skills/backend-ui-design/SKILL.md`](../backend-ui-design/SKILL.md) — DS-compliant pages
@@ -19,26 +19,42 @@ Every item must be answered in the spec or marked N/A with justification.
19
19
 
20
20
  ## 3. Data Integrity & Security
21
21
 
22
- - [ ] Entities include `id`, `organization_id`, `created_at`, `updated_at`
22
+ - [ ] Entities include `id`, `organization_id`, `tenant_id`, `created_at`, `updated_at`, `deleted_at`, `is_active`
23
23
  - [ ] Write operations define transaction boundaries
24
24
  - [ ] Input validation uses zod schemas
25
25
  - [ ] All user input validated before business logic/persistence
26
- - [ ] Auth guards are declared (`requireAuth`, `requireRoles`, `requireFeatures`)
27
- - [ ] Tenant isolation: every scoped query filters by `organization_id`
26
+ - [ ] Auth guards declared per-method in `metadata` (`requireAuth`, `requireFeatures`) — never legacy top-level `export const requireAuth`
27
+ - [ ] Tenant isolation: every scoped query filters by `organization_id` (and `tenant_id` where applicable)
28
+ - [ ] **Encryption maps mechanism is used (no hand-rolled crypto).** For every PII / GDPR-relevant column the spec proposes — names, addresses, contacts, free-text notes about people, integration credentials, secrets, document numbers — the spec MUST declare them in a module-level `<module>/encryption.ts` exporting `defaultEncryptionMaps: ModuleEncryptionMap[]` (type from `@open-mercato/shared/modules/encryption`). Reads MUST go through `findWithDecryption` / `findOneWithDecryption` (5-arg `(em, entity, where, options?, scope?)`) from `@open-mercato/shared/lib/encryption/find`. Equality-lookup columns (e.g. login email) declare a sibling `hashField`. No `crypto.subtle`, no custom KMS calls, no "TODO encrypt later". See `AGENTS.md` → CRITICAL Rule #11 (Encryption maps) + the "Encryption maps for sensitive data" row of the Mandatory Module Mechanisms table; `.ai/skills/data-model-design/SKILL.md` § Sensitive Data and Encryption Maps.
28
29
 
29
30
  ## 4. Commands, Events & Naming
30
31
 
31
32
  - [ ] Naming is singular and consistent
32
33
  - [ ] All mutations are commands with undo logic
33
- - [ ] Events declared in `events.ts` before emitting
34
+ - [ ] Events declared in `<module>/events.ts` via `createModuleEvents({ moduleId, events } as const)` before emitting; cross-module side effects use `subscribers/*.ts`, never direct cross-module imports
34
35
  - [ ] Side-effect reversibility is documented
35
36
 
36
- ## 5. API & UI
37
+ ## 5. API & UI — Canonical Mechanisms
37
38
 
38
39
  - [ ] API contracts are complete (request/response/errors)
39
40
  - [ ] Routes include `openApi` expectations
40
- - [ ] UI uses `CrudForm`, `DataTable`, and shared primitives
41
- - [ ] i18n keys are planned for user-facing strings
41
+ - [ ] **Canonical mechanisms no DIY substitutes.** The spec MUST reach for the framework primitives, not invent its own. See `AGENTS.md` **Mandatory Module Mechanisms**.
42
+ - [ ] **CRUD APIs** use `makeCrudRoute({ entity, entityId, operations, schema, indexer: { entityType } })` from `@open-mercato/shared/lib/crud/factory`. Custom write routes call `validateCrudMutationGuard` before mutation and `runCrudMutationGuardAfterSuccess` after.
43
+ - [ ] **API route files export `metadata`** with per-method `requireAuth` / `requireFeatures` (no top-level `export const requireAuth`).
44
+ - [ ] **Backend forms** use `<CrudForm>` from `@open-mercato/ui/backend/CrudForm` with helpers `createCrud` / `updateCrud` / `deleteCrud` from `@open-mercato/ui/backend/utils/crud`, throwing `createCrudFormError` from `@open-mercato/ui/backend/utils/serverErrors` for field-level errors. No raw `<form>`, no raw `fetch`.
45
+ - [ ] **Lists** use `<DataTable entityId apiPath columns />` from `@open-mercato/ui/backend/DataTable` with stable `entityId` / `extensionTableId` so widget injection (columns / row actions / bulk actions / filters / toolbar) keeps working.
46
+ - [ ] **HTTP clients** use `apiCall` / `apiCallOrThrow` / `readApiResultOrThrow` from `@open-mercato/ui/backend/utils/apiCall` — never raw `fetch`.
47
+ - [ ] **Non-`CrudForm` writes** are wrapped in `useGuardedMutation(...).runMutation(...)` and pass `retryLastMutation` in the injection context.
48
+ - [ ] **Cache** is resolved via DI (`container.resolve('cache')`) — never `new Redis(...)` or raw SQLite. Tags include `tenant:<id>` / `org:<id>`.
49
+ - [ ] **Design System compliance for every UI mock and className snippet in the spec.** See `AGENTS.md` → CRITICAL Rule #10 (Strict Design System alignment) and `.ai/skills/backend-ui-design/SKILL.md`.
50
+ - [ ] Use semantic status tokens (`text-status-error-text`, `bg-status-success-bg`, `border-status-warning-border`, `text-status-info-icon`, `text-destructive`, `bg-destructive`) — NEVER hardcoded shades like `text-red-500`, `bg-green-100`, `text-amber-*`, `text-emerald-*`, `bg-blue-*`. Status tokens already cover dark mode; no `dark:` overrides.
51
+ - [ ] Use the Tailwind text scale (`text-xs` 12, `text-sm` 14, `text-base` 16, `text-lg` 18, `text-xl` 20, `text-2xl` 24) or `text-overline` for 11px uppercase labels — NEVER arbitrary sizes (`text-[11px]`, `text-[13px]`, `text-[15px]`, `p-[13px]`, `rounded-[24px]`, `z-[9999]`).
52
+ - [ ] Use shared primitives instead of raw HTML: `<Alert variant=...>` for inline status, `flash('msg', 'success|error|warning|info')` for toasts, `useConfirmDialog()` for destructive confirmations, `<StatusBadge>` for entity status, `<FormField label error>` to wrap form inputs, `<SectionHeader title count action>` for section headers, `<CollapsibleSection>` for collapsible regions, `<LoadingMessage>` / `<Spinner>` / `<DataLoader>` for async states, `<EmptyState>` (or DataTable `emptyState` prop) for empty lists.
53
+ - [ ] Use lucide-react icons in PAGE BODY UI (`Page`, `DataTable`, `CrudForm`, cards, buttons) — never inline `<svg>`. Sizes from the `size-{3|4|5|6}` scale; `strokeWidth` is not overridden per-instance. `page.meta.ts` icons follow the `React.createElement('svg', …)` pattern.
54
+ - [ ] Every dialog supports `Cmd/Ctrl+Enter` to submit and `Escape` to cancel.
55
+ - [ ] Every icon-only button has an `aria-label`.
56
+ - [ ] Boy Scout rule: when the spec edits an existing page, any line touched gets migrated to semantic tokens / DS scale.
57
+ - [ ] i18n keys are planned for user-facing strings (`useT()` client-side, `resolveTranslations()` server-side; never hard-coded labels)
42
58
  - [ ] Pagination limits defined (`pageSize <= 100`)
43
59
 
44
60
  ## 6. Risks & Anti-Patterns
@@ -48,3 +64,4 @@ Every item must be answered in the spec or marked N/A with justification.
48
64
  - [ ] Does not introduce cross-module ORM links
49
65
  - [ ] Does not skip undoability for state changes
50
66
  - [ ] Does not mix MVP with speculative future phases
67
+ - [ ] Does not introduce hand-rolled AES, raw `fetch`, raw `<form>`, `new Redis(...)`, or arbitrary Tailwind sizes / status colors
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-mercato-app",
3
- "version": "0.6.1-develop.3090.06ab462170",
3
+ "version": "0.6.1-develop.3102.d6e7e6d57a",
4
4
  "type": "module",
5
5
  "description": "Create a new Open Mercato application",
6
6
  "main": "./dist/index.js",
@@ -62,9 +62,12 @@ mercato test coverage
62
62
  # Generate code from modules
63
63
  yarn generate
64
64
 
65
- # Manually purge structural navigation/sidebar caches when needed
65
+ # Manually purge structural caches when needed (Redis nav:* + Turbopack barrel mtimes)
66
66
  yarn mercato configs cache structural --all-tenants
67
67
 
68
+ # Escape hatch: clear .next/cache/turbopack when Turbopack still serves a stale chunk
69
+ yarn dev:reset
70
+
68
71
  # Database operations
69
72
  yarn db:generate # Generate/probe migrations; keep or write only scoped SQL and update the touched snapshot
70
73
  yarn db:migrate # Run migrations
@@ -210,7 +213,7 @@ Practical consequences:
210
213
  Standalone apps consume the AI framework from `@open-mercato/ai-assistant` (in `node_modules/`). The same conventions used in the monorepo apply here:
211
214
 
212
215
  - Add a typed agent for a new module by creating `<module>/ai-agents.ts` + `<module>/ai-tools.ts` at the **module root**. Run `yarn generate` after.
213
- - Add inline UI widgets (record cards, custom server-emitted parts) per the [UI Parts guide](https://docs.openmercato.dev/framework/ai-assistant/ui-parts).
216
+ - Add inline UI widgets (record cards, custom server-emitted parts) per the [UI Parts guide](https://docs.open-mercato.dev/framework/ai-assistant/ui-parts).
214
217
  - Replace or disable an agent / tool that another module shipped through three paths: extra `aiAgentOverrides` / `aiToolOverrides` exports on the existing `<module>/ai-agents.ts` / `<module>/ai-tools.ts` (per-module), inline on a `ModuleEntry` in `src/modules.ts` (per-app), or programmatically via `applyAiAgentOverrides({...})` / `applyAiToolOverrides({...})` from `@open-mercato/ai-assistant`. `null` disables; a definition replaces. Resolution order is **programmatic → modules.ts → file-based → base**.
215
218
 
216
219
  Example per-module override (preferred when the override should ship with a module):
@@ -320,6 +323,80 @@ Do this automatically unless the user has explicitly said otherwise. If the curr
320
323
 
321
324
  Feature IDs are FROZEN once shipped (they are stored in the DB as `role_features.feature_id`). If a rename is required, add the new ID, grant it, and keep the old one alongside as a deprecated alias until downstream data can be migrated.
322
325
 
326
+ ## Mandatory Module Mechanisms (no DIY substitutes)
327
+
328
+ When building a new application or a new module under `src/modules/<id>/`, do not invent custom routing, auth, persistence, forms, or caching. The framework provides one canonical primitive for each concern. If a feature is not on this list, ask before adding it.
329
+
330
+ | Concern | Canonical mechanism | Reference |
331
+ |---|---|---|
332
+ | Module structure & auto-discovery | `src/modules/<id>/{api,backend,frontend,data,subscribers,workers,widgets}` + `index.ts` + `src/modules.ts` (`from: '@app'`); discovered by `yarn generate` | <https://docs.open-mercato.dev/framework/modules/overview> |
333
+ | Backend admin pages | Auto-discovered files under `backend/**` with paired `page.meta.ts` (`requireAuth`, `requireFeatures`, `pageGroup`, `pageGroupKey`, `pageOrder`) | <https://docs.open-mercato.dev/framework/modules/routes-and-pages> |
334
+ | Frontend public pages and customer portal | Auto-discovered files under `frontend/**`. Portal pages live at `frontend/[orgSlug]/portal/<path>/page.tsx` with `requireCustomerAuth` / `requireCustomerFeatures` | <https://docs.open-mercato.dev/framework/modules/routes-and-pages> |
335
+ | API routes (auth + OpenAPI) | `src/modules/<id>/api/**/route.ts` exporting handlers + `metadata` (per-method `requireAuth` / `requireFeatures`) + `openApi` | <https://docs.open-mercato.dev/framework/api/api-development-guide> |
336
+ | CRUD APIs (factory) | `makeCrudRoute({ entity, entityId, operations, schema, indexer: { entityType } })` from `@open-mercato/shared/lib/crud/factory` | <https://docs.open-mercato.dev/framework/api/crud-factory> |
337
+ | CRUD forms in admin | `<CrudForm entityId apiPath mode fields />` from `@open-mercato/ui/backend/CrudForm`; helpers `createCrud` / `updateCrud` / `deleteCrud` from `@open-mercato/ui/backend/utils/crud`; `createCrudFormError` from `@open-mercato/ui/backend/utils/serverErrors`. Never raw `<form>` or raw `fetch` | <https://docs.open-mercato.dev/framework/admin-ui/crud-form> |
338
+ | DataTables in admin | `<DataTable entityId apiPath columns />` from `@open-mercato/ui/backend/DataTable`; keep `entityId` and `extensionTableId` stable so widget injection (columns, row actions, filters, toolbar) keeps working | <https://docs.open-mercato.dev/framework/admin-ui/data-grids> |
339
+ | Authorization (RBAC) | Declare features in `<module>/acl.ts`, grant in `<module>/setup.ts` `defaultRoleFeatures`, gate routes/pages with `requireFeatures` in `metadata`. NEVER use `requireRoles`. Run `yarn mercato auth sync-role-acls` after adding features | <https://docs.open-mercato.dev/framework/rbac/overview> |
340
+ | Multi-tenant scoping (default) | Every tenant-scoped entity MUST include indexed `organization_id` and `tenant_id`; every read/write filters by them. The CRUD factory injects the scope automatically — do not bypass it | <https://docs.open-mercato.dev/architecture/system-overview> |
341
+ | **Encryption maps for sensitive data** | Declare `<module>/encryption.ts` exporting `defaultEncryptionMaps: ModuleEncryptionMap[]`; read via `findWithDecryption` / `findOneWithDecryption`. NEVER hand-roll AES/KMS — see the next section | <https://docs.open-mercato.dev/user-guide/encryption> |
342
+ | Cache | Resolve from DI (`container.resolve('cache')`); never `new Redis(...)` or raw SQLite. Tag with `tenant:<id>` / `org:<id>` for tenant-scoped invalidation | <https://docs.open-mercato.dev/user-guide/cache-management> |
343
+ | Background workers | `src/modules/<id>/workers/*.ts` exporting `metadata: { queue, id?, concurrency? }` + default handler. Never spin up custom queues | <https://docs.open-mercato.dev/framework/events/queue-workers> |
344
+ | Events between modules | `<module>/events.ts` with `createModuleEvents({ moduleId, events } as const)`; subscribers in `subscribers/*.ts` | <https://docs.open-mercato.dev/framework/events/overview> |
345
+ | i18n (every user-facing string) | `useT()` client-side from `@open-mercato/shared/lib/i18n/context`, `resolveTranslations()` server-side from `@open-mercato/shared/lib/i18n/server`; keys in `src/i18n/<locale>.json` | The `@open-mercato/shared` package (`node_modules/@open-mercato/shared/lib/i18n/`) |
346
+
347
+ > Rule of thumb: if you reach for raw `fetch`, raw `<form>`, ad-hoc `crypto`, ad-hoc `Redis`, or a manual cross-module ORM join, stop and check the row above first.
348
+
349
+ ## Data Encryption (sensitive / GDPR-relevant fields)
350
+
351
+ The framework ships a tenant-data-encryption mechanism with per-tenant DEKs, KMS-backed key resolution (Vault by default), declarative field-level maps per module, and deterministic-hash sibling columns for equality lookups. **Use it. Never hand-roll AES, `crypto.subtle`, or custom KMS calls. Never store sensitive columns as plaintext "for now".**
352
+
353
+ When the user asks for "we need this column encrypted", "store this securely", "this is PII", "GDPR", or "encryption at rest" — and whenever you are designing a column that holds names, addresses, contact info, free-text notes about people, integration credentials, secrets, or anything subject to a data-processing agreement — declare an `encryption.ts` at the module root.
354
+
355
+ ```ts
356
+ // src/modules/<module>/encryption.ts
357
+ import type { ModuleEncryptionMap } from '@open-mercato/shared/modules/encryption'
358
+
359
+ export const defaultEncryptionMaps: ModuleEncryptionMap[] = [
360
+ {
361
+ entityId: '<module>:<entity>',
362
+ fields: [
363
+ { field: 'first_name' },
364
+ { field: 'last_name' },
365
+ { field: 'phone' },
366
+ // For deterministic equality lookups (e.g. login by email), add a sibling hash column.
367
+ { field: 'email', hashField: 'email_hash' },
368
+ ],
369
+ },
370
+ ]
371
+
372
+ export default defaultEncryptionMaps
373
+ ```
374
+
375
+ Read with decryption — never raw `em.find` / `em.findOne` on encrypted columns:
376
+
377
+ ```ts
378
+ import { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
379
+
380
+ // Signature: (em, entityName, where, options?, scope?). Pass MikroORM FindOptions in slot 4
381
+ // (or `undefined`), and the decryption scope in slot 5.
382
+ const records = await findWithDecryption(em, '<Entity>', filter, undefined, { tenantId, organizationId })
383
+ const single = await findOneWithDecryption(em, '<Entity>', { id }, undefined, { tenantId, organizationId })
384
+ ```
385
+
386
+ Apply maps to existing tenants after declaring them (new tenants pick them up automatically during `auth:setup`):
387
+
388
+ ```bash
389
+ yarn mercato entities seed-encryption --tenant <tenantId> [--organization <orgId>]
390
+ ```
391
+
392
+ Notes:
393
+
394
+ - Toggling the **Encrypted** flag on a custom field via the admin UI only applies to data written *after* the change. Backfill historical plaintext rows with `yarn mercato entities rotate-encryption-key --tenant <tenantId> --org <organizationId>` (without `--old-key` it only encrypts plaintext and skips already-encrypted fields). Use `yarn mercato entities decrypt-database` to roll back.
395
+ - The `vector` module stores raw embeddings unencrypted in the vector store — treat embeddings as sensitive even when the source text is encrypted.
396
+ - Env switches: `TENANT_DATA_ENCRYPTION` (default `yes`), `TENANT_DATA_ENCRYPTION_DEBUG`, Vault (`VAULT_ADDR` / `VAULT_TOKEN` / `VAULT_KV_PATH`), and dev fallback (`TENANT_DATA_ENCRYPTION_FALLBACK_KEY`).
397
+
398
+ Full guide: <https://docs.open-mercato.dev/user-guide/encryption>.
399
+
323
400
  ## Design System (Strict — applies to every UI change)
324
401
 
325
402
  All UI added or edited in `src/modules/<module>/backend/**` or `src/modules/<module>/frontend/**` MUST follow the Open Mercato design system. Non-compliant code will be blocked in `auto-review-pr`.
@@ -401,6 +478,8 @@ Notes:
401
478
 
402
479
  The standalone template enables the `configs` module from `@open-mercato/core`, so `yarn mercato configs cache ...` is available here after installation. After structural changes such as enabling or disabling modules, adding or removing backend/frontend pages, or changing sidebar/navigation injections, run `yarn generate`. The generator now performs a best-effort structural cache purge automatically after successful generation; if the cache command is unavailable, generation still succeeds.
403
480
 
481
+ The structural cache purge invalidates two layers: Redis `nav:*` cache keys and Turbopack's module-graph fingerprints (it bumps mtimes on every file in `.mercato/generated/` without changing content). When Turbopack still serves a stale compiled chunk after a structural change — typically because its own internal cache pinned a previous compile error — run `yarn dev:reset` to clear `.next/cache/turbopack` and restart `yarn dev`.
482
+
404
483
  Detail/read-model APIs that expose `customFields` must return bare field keys via `normalizeCustomFieldResponse()` (for example `{ priority: 3 }`). Keep `cf_` / `cf:` prefixes for request payloads, filters, and form field IDs only.
405
484
 
406
485
  ### Path Aliases
@@ -12,6 +12,7 @@
12
12
  "dev": "node ./scripts/dev.mjs",
13
13
  "dev:classic": "node ./scripts/dev.mjs --classic",
14
14
  "dev:verbose": "node ./scripts/dev.mjs --verbose",
15
+ "dev:reset": "node ./scripts/dev-reset.mjs",
15
16
  "build": "yarn generate && next build",
16
17
  "start": "mercato server start",
17
18
  "lint": "next lint",
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ import { fileURLToPath } from 'node:url'
3
+ import path from 'node:path'
4
+ import fs from 'node:fs'
5
+
6
+ const here = path.dirname(fileURLToPath(import.meta.url))
7
+ const appDir = path.resolve(here, '..')
8
+ const targets = [
9
+ path.join(appDir, '.next', 'cache', 'turbopack'),
10
+ path.join(appDir, '.next', 'cache', 'webpack'),
11
+ ]
12
+
13
+ let removed = 0
14
+ for (const target of targets) {
15
+ if (!fs.existsSync(target)) continue
16
+ fs.rmSync(target, { recursive: true, force: true })
17
+ console.log(`🧹 [dev:reset] removed ${path.relative(appDir, target)}`)
18
+ removed += 1
19
+ }
20
+
21
+ if (removed === 0) {
22
+ console.log('🧹 [dev:reset] nothing to clean — .next/cache subdirectories already absent')
23
+ }
24
+
25
+ console.log('')
26
+ console.log('✅ Turbopack/webpack cache cleared.')
27
+ console.log(' Stop any running `yarn dev` and start it again to pick up fresh module output.')