Instalar com Codex ou Claude Copie este prompt, cole no Codex, Claude ou outro assistente e deixe que ele revise a página da skill e instale para você.
Um comando direto ignora o prompt de revisão. Verifique a origem antes de executá-lo.
Clarify entities — What are the distinct "things" being stored?
Clarify fields — What data does each entity hold?
Clarify relationships — How do entities relate? (1:1, 1:N, N:M, cross-module?)
Choose patterns — Select the right pattern for each relationship
Generate — Create entity files, validators, and migrations
Verify — Check migration output, test queries
2. Entity Design
Standard Entity Template
Define entities in src/modules/<module_id>/data/entities.ts. Standalone apps keep the module's entity classes together there unless the file becomes large enough that a split is justified.
Critical rule: NO ORM relationships (@ManyToOne, @OneToMany) between entities in different modules.
Pattern: FK ID Only
@Entity({ tableName: 'tickets' })
exportclassTicket {
// Reference to customer in another module — just a UUID column@Index()
@Property({ type: 'uuid' })
customer_id!: string// FK to customers.person — NO @ManyToOne// Reference to assigned user in auth module@Index()
@Property({ type: 'uuid', nullable: true })
assigned_to: string | null = null// FK to auth.user
}
Fetching Related Data
To display related data from another module, use a Response Enricher (see system-extension skill):
Module isolation — modules must be independently deployable and ejectable
Circular dependencies — ORM relations create tight coupling between modules
Schema ownership — each module owns its entities; cross-module ORM relations blur ownership
Extension system — UMES enrichers provide the same capability without coupling
6. Migration Lifecycle
Creating a Migration
# 1. Modify src/modules/<module_id>/data/entities.ts# 2. Probe/generate migration
yarn db:generate
# 3. Review the generated migration or use it as the baseline for scoped manual SQL# Check src/modules/<module_id>/migrations/Migration_YYYYMMDD_HHMMSS.ts# 4. Update src/modules/<module_id>/migrations/.snapshot-open-mercato.json# 5. Apply migration only after explicit user confirmation
yarn db:migrate
Migration Best Practices
Review every migration — auto-generated doesn't mean correct
Check for unintended changes — sometimes generators pick up unrelated diffs
Do not commit unrelated generated migrations — delete them from the diff
Scoped manual SQL is allowed when generator churn is unrelated, but the migration and .snapshot-open-mercato.json must still describe the same post-change schema
Update .snapshot-open-mercato.json — it is the baseline that prevents duplicate future migrations
New columns should have defaults — prevents breaking existing rows
Never rename columns — add new column, migrate data, remove old column (across releases)
Never drop tables — soft delete or archive first
Adding a Column to Existing Entity
// Add to entity with a default value@Property({ type: 'varchar', length: 100, default: '' })
new_field: string = ''// Or nullable for optional fields@Property({ type: 'varchar', length: 100, nullable: true })
new_field: string | null = null
Then:
yarn db:generate # Probes/creates ALTER TABLE ADD COLUMN migration
yarn db:migrate # Applies it only after explicit user confirmation
Removing a Column
Don't remove columns in a single step. Instead:
Stop writing to the column (remove from validators and forms)
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.
The mechanism gives you:
Per-tenant Data Encryption Keys (DEKs) resolved through the configured KMS (Vault by default, env-fallback in dev).
Declarative, per-entity, per-field encryption with optional deterministic-hash sibling columns for equality lookups (for example login by email).
Boot-time auto-application: every enabled module's defaultEncryptionMaps is collected during auth:setup and applied when TENANT_DATA_ENCRYPTION=yes.
A findWithDecryption / findOneWithDecryption read API that transparently decrypts on read.
When encryption is mandatory
Field example
Encrypt?
First name, last name, preferred name
Yes
Email, phone
Yes — usually with a hashField for lookups
Postal address (line 1/2, city, region, postal code, country)
Yes
Free-text comments / notes / activity bodies that mention people
Yes
Integration secrets, API keys, OAuth tokens, webhook signing keys
Yes
Document numbers (tax IDs, national IDs)
Yes
Status enums, counters, timestamps, FKs, currency codes
No
Public catalog metadata (product titles for a public storefront)
Usually no
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.
Declare the map in <module>/encryption.ts
importtype { ModuleEncryptionMap } from'@open-mercato/shared/modules/encryption'exportconstdefaultEncryptionMaps: ModuleEncryptionMap[] = [
{
entityId: '<module_id>:<entity>', // matches the entity's table id (colon-separated)fields: [
{ field: 'first_name' },
{ field: 'last_name' },
{ field: 'phone' },
// Sibling deterministic hash for equality lookups (e.g. login by email).// Add a matching `<field>_hash varchar` column to the entity.
{ field: 'email', hashField: 'email_hash' },
],
},
]
exportdefault defaultEncryptionMaps
Read with decryption — never raw em.find
import { findWithDecryption, findOneWithDecryption } from'@open-mercato/shared/lib/encryption/find'// Signature: (em, entityName, where, options?, scope?). MikroORM FindOptions go in slot 4// (pass `undefined` if you have none), the decryption scope `{ tenantId, organizationId }` in slot 5.const records = awaitfindWithDecryption(em, '<Entity>', filter, undefined, { tenantId, organizationId })
const single = awaitfindOneWithDecryption(em, '<Entity>', { id }, undefined, { tenantId, organizationId })
Calling em.find on an encrypted column returns ciphertext, breaks search, and silently leaks bug surface. The findWithDecryption family is the one entry point.
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.
Vector search caveat
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.
Environment switches
TENANT_DATA_ENCRYPTION=yes|no (default yes) — set to no to run the hooks as no-op (validation still applies).
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.
9. Anti-Patterns
Anti-Pattern
Problem
Correct Pattern
@ManyToOne across modules
Tight coupling, breaks module isolation
Store FK as uuid column, use enrichers
Storing computed values
Stale data, maintenance burden
Compute on read via enrichers or queries
Using any for JSONB fields
No type safety
Define a Zod schema, use z.infer
Blindly committing all generated migrations
Captures unrelated snapshot drift
Keep only scoped SQL and update the matching snapshot
Manual migration SQL without snapshot update
Future yarn db:generate recreates the same migration
Update .snapshot-open-mercato.json in the same change
Renaming columns
Breaks existing data/queries
Add new column, migrate data, drop old
Missing organization_id
Cross-tenant data leaks
Always include and index
Using varchar without length
Defaults vary by DB
Always specify length
Storing arrays as comma-separated strings
Can't query, no integrity
Use jsonb arrays or junction tables
UUID FK without index
Slow joins
Always @Index() on FK columns
Nullable required fields
Data integrity issues
Use ! assertion for required, null for optional
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
Reading encrypted columns with raw em.find / em.findOne
Returns ciphertext, breaks search, silent data corruption
Use findWithDecryption / findOneWithDecryption with { tenantId, organizationId }
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
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
Rules
MUST include organization_id and tenant_id on all tenant-scoped entities
MUST include standard columns (id, created_at, updated_at, deleted_at, is_active)
MUST use UUID v4 for primary keys
MUST index all FK columns and organization_id / tenant_id
MUST create or keep a scoped migration after entity changes and update .snapshot-open-mercato.json
MUST review generated migration before applying
MUST NOT commit unrelated migrations emitted by yarn db:generate
MUST NOT run yarn db:migrate without explicit user confirmation
MUST use nullable: true with = null default for optional fields
MUST specify length on all varchar columns
MUST NOT use ORM relationship decorators across module boundaries
MUST NOT rename or drop columns in a single release
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
MUST NOT hand-roll AES / KMS calls or store sensitive columns as plaintext "for now" — use the encryption-maps mechanism in section 8
Use jsonb for flexible/nested data, proper columns for queryable/sortable data
Use junction tables for many-to-many relationships
Derive TypeScript types from Zod schemas, never duplicate type definitions