8.4 KiB
Module Conventions
This document establishes the structural conventions governing all modules in the modules/ directory. It is the normative reference for module authorship and serves as the style guide for maintaining consistency across modules of varying size and complexity.
Directory Structure
Each module follows a uniform five-file structure:
modules/
<module-name>/
README.md — Overview, table list, quick links
model.md — DDL + table descriptions (projected from data-model.md)
companion.md — Module-scoped decisions and open issues
architecture.md — Encyclopedic documentation (adapted from synthesis docs)
interfaces.md — Cross-module interface documentation
No file is optional. Even modules with minimal cross-module interfaces (e.g., integration) include an interfaces.md that documents their isolation.
Relationship to Root-Level Documents
data-model.md (Master Reference)
The master data model reference is retained and authoritative. It serves a different purpose (complete, searchable, printable) than the modular structure (navigable, maintainable). Module model.md files are projections of the master reference, not forks. When the master reference is updated, the corresponding module model.md must be updated in the same change.
companion.md (Master Companion)
The master companion retains all decisions with their original numbering. Module companion.md files reference root companion decision numbers — they do not duplicate the decision text. Each module companion provides:
- A table listing the decision numbers relevant to that module, with one-line summaries
- Module-scoped open issues (if any)
- Module-specific architectural notes
documents/ (Historical Artifacts)
The documents/ directory remains as historical artifacts. Module architecture.md files synthesize from these documents but do not replace them. The architecture files are adapted, restructured, and contextualized for the module's scope.
Schema Naming Convention
Each module occupies its own PostgreSQL schema, matching the module directory name (Decision 113):
| Module | Schema |
|---|---|
| identity | identity |
| organization | organization |
| entitlements | entitlements |
| billing | billing |
| cooperative | cooperative |
| audit | audit |
| integration | integration |
Integration provider schemas (stripe, polar, nextcloud, etc.) follow Decision 86 and coexist alongside module schemas. The public schema holds shared extensions and functions only.
Per-Schema Role Model (Decision 115)
Each schema receives three non-login roles as recommended practice (binding when a runtime database is provisioned):
| Role | Convention | Purpose |
|---|---|---|
| Owner | {schema}_owner |
DDL operations, migration execution |
| Writer | {schema}_writer |
Runtime read-write operations |
| Reader | {schema}_reader |
Cross-module read access, reporting |
Cross-module read access is granted via reader role inheritance: a module's writer role inherits the reader role of modules it depends on (e.g., entitlements_writer inherits organization_reader and billing_reader).
Cross-Module FK Annotation Format
When a table references an entity in another module, the FK annotation uses schema-qualified dot notation (Decision 114):
FK -> module.table_name
Examples:
FK -> identity.persons — references persons table in the identity schema
FK -> billing.billing_accounts — references billing_accounts in the billing schema
FK -> organization.organizations — references organizations in the organization schema
Intra-module FK references omit the schema prefix:
FK -> table_name — references a table within the same module/schema
This annotation appears in model.md table descriptions (in the Relationships section) and in interfaces.md (in the interface inventory).
Decision Reference Format
Module companions reference root companion decisions by number:
Decision 42 (Temporal Modeling Strategy) — see root companion
Module companions do not duplicate full decision text. They provide a one-line summary and a pointer to the root companion for the authoritative record. This prevents drift between root and module companions.
Module-scoped decisions (if any arise during module evolution) are numbered in a module-local sequence prefixed with the module abbreviation:
ID-1: Module-local decision for identity
ORG-1: Module-local decision for organization
At present, no module-local decisions exist. All decisions are recorded in the root companion.
Cross-Cutting Policies
The following policies govern behavior across all modules. They are documented at root level and referenced, not duplicated in module files:
| Policy | Location | Governs |
|---|---|---|
| Soft-Delete and Terminal State Policy | documents/policy-soft-delete-terminal-state.md |
All status-bearing tables, FK behavior, GDPR anonymization, person merge |
| JSONB Governance Policy | documents/policy-jsonb-governance.md |
All JSONB columns |
| Primary Key Strategy (UUIDv7) | data-model.md §Primary Key Strategy |
All primary keys |
| Temporal Modeling Convention | data-model.md §Structural Policies |
All status timestamps and actor attribution |
| Polymorphic Pattern Policy | data-model.md §Polymorphic Pattern Policy |
All polymorphic relationships |
Module model.md files may note where a cross-cutting policy applies (e.g., "Status lifecycle governed by the Soft-Delete and Terminal State Policy") but must not reproduce the policy text.
interfaces.md Structure
Each interfaces.md documents cross-module relationships from the module's perspective:
Provided Interfaces (other modules depend on us)
Tables and columns that other modules reference via FK. For each:
- Which modules consume it
- The FK column(s)
- Cardinality and semantics
Consumed Interfaces (we depend on other modules)
Tables and columns in other modules that this module references via FK. For each:
- Which module provides it
- The FK column(s)
- Why the dependency exists
Interface Contracts
Behavioral expectations that cross module boundaries. Examples:
- "The identity module guarantees that
persons.person_idremains stable through anonymization — the row is mutated, not deleted." - "The billing module guarantees that
products.product_idremains stable when a product is deactivated."
README.md Structure
Each module README follows this template:
# <Module Name>
**Schema:** `<module_name>`
**Type:** Domain / Cross-cutting
**Tables:** <count>
**Primary source:** <synthesis doc reference>
**Decisions:** <list of root companion decision numbers>
## Purpose
<One paragraph answering the module's core question>
## Tables
| Table | Purpose |
|-------|---------|
| ... | ... |
## Quick Links
- [Model Reference](model.md) — DDL and table descriptions
- [Architecture](architecture.md) — Deep-dive documentation
- [Module Companion](companion.md) — Decisions and open issues
- [Interfaces](interfaces.md) — Cross-module dependencies
model.md Content
Module model.md files contain the exact DDL and table descriptions from the corresponding section of data-model.md. They are projections — the text is adapted for standalone readability (e.g., section headings adjusted) but the technical content is identical to the master reference.
Each table entry includes:
- Table description (purpose, design rationale)
- Field table (field, type, purpose)
- Constraints (CHECK, UNIQUE, partial indexes)
- Status lifecycle (if applicable)
- Relationships (with cross-module FK annotations)
architecture.md Content
Module architecture.md files synthesize from the corresponding documents/doc-* synthesis document(s). They are adapted, not copied: restructured for the module's scope, updated to reflect the current state of the model, and contextualized with cross-module awareness.
Key differences from source synthesis documents:
- Section structure may differ to serve module navigation
- Cross-module interactions are documented inline rather than as external references
- Cooperative-specific concerns (in billing) are documented as a tagged sub-domain
- Content is current — any post-synthesis decisions are incorporated