Deep dive into our 1,700+ line Prisma schema with organization hierarchies, RBAC, event sourcing, and modular systems that scale from single-property to enterprise.
The Scale Challenge
Building a property management platform sounds straightforward until you encounter the scope: multiple organizations, each with multiple properties, each with multiple units, each with multiple residents, each with multiple vehicles, payments, maintenance requests, and lease documents.
The schema eventually exceeded 100 models. The question became: how do you structure data at this scale without drowning in complexity?
Hierarchy as Architecture
The first insight is that organizational hierarchy isn't just a business requirement—it's an architectural principle. Everything in the system belongs to something: units belong to properties, properties belong to organizations, residents belong to units.
This containment relationship determines data access, permission boundaries, and query patterns. Get the hierarchy right, and everything else follows. Get it wrong, and you're fighting the structure at every turn.
We settled on a three-level organization model: organizations own properties, properties contain units. Each level has its own permission boundaries and administrative scope.
Tenant Isolation
Multi-tenancy means multiple customers share infrastructure while seeing only their own data. The critical question is where to draw the isolation boundary.
We chose property-level isolation. Every query that touches business data filters by property ID. Cross-property access requires explicit grants—there's no accidental data leakage because the default is denial.
This is implemented through a combination of database-level constraints (foreign keys, composite unique constraints) and application-level enforcement (every service method requires a tenant context). The redundancy is intentional: defense in depth for data isolation.
The Module Contract
With 100+ models, we needed organizational structure beyond the schema itself. We developed a formal module contract: a standardized interface that every functional domain must implement.
Each module declares its identity, dependencies, data models, API surface, events it emits and handles, UI components, settings, lifecycle hooks, health checks, and documentation. The contract makes modules self-describing and loosely coupled.
The parking module doesn't import anything from leasing. It emits events that leasing can handle if it wants to. This inversion—dependencies flowing through events rather than direct imports—prevents the spaghetti that kills large codebases.
Permission Granularity
Role-based access control spans multiple organizational levels. A user might be an owner at the organization level, an admin at one property, and a viewer at another.
Permissions are defined as atoms: "can view residents," "can edit lease terms," "can process payments." Roles are collections of permissions. Users have roles scoped to specific properties.
The permission check becomes: for this user, at this property, does their role include the required permission? The answer depends on three lookups, but those lookups are fast and cacheable.
Event Sourcing for Audit
Every significant operation—lease changes, payment processing, access modifications—emits an audit event. The event captures who, what, when, and the before/after state.
This isn't just for compliance. It's for debugging, for customer support, for understanding how data got into unexpected states. When something goes wrong, the audit trail tells the story.
The events are append-only. You can't edit history. This immutability is a feature: it provides a trustworthy record of everything that happened.
Query Patterns
The hierarchy enables predictable query patterns. "Show me everything for this property" is a natural request, and the schema supports it efficiently. Property ID indexes on every relevant table make these queries fast.
Dashboard queries aggregate across the hierarchy: total units, occupancy rate, revenue this month, maintenance backlog. These can run in parallel—each aggregate is independent—and respond in sub-100ms even at scale.
The key is designing indexes during schema creation, not after performance problems appear. Knowing your query patterns in advance means knowing your index requirements.
Lessons in Schema Design
Several principles emerged from this project.
First, explicit is better than implicit. Every relationship is declared with foreign keys and constraints. There are no "magic" connections inferred at runtime.
Second, normalize until it hurts, then denormalize strategically. The core schema is highly normalized—no data duplication. But we add denormalized views and materialized data where read performance demands it.
Third, treat the schema as documentation. If you can't explain the data model to a new engineer, you can't maintain the application. Clear naming, consistent patterns, and self-evident relationships matter.
Fourth, audit everything meaningful. Storage is cheap. Investigation time during incidents is expensive. The audit trail pays for itself many times over.
A 100+ model schema sounds daunting. But with clear hierarchy, consistent patterns, and explicit contracts between modules, it remains manageable. The structure does the work.