Ecopick — Complete Architecture Reference
Purpose: Self-contained reference for the entire Ecopick system. Covers every model, every API route, every permission rule, every state machine, and every data flow. Intended as context for LLM prompts — nothing is omitted.
1. Overview
Ecopick is a building operations orchestration platform (B2B2C) that streamlines the complete lifecycle: tenant/occupant request → triage → routing → dispatch → SLA enforcement → evidence → closure. It gives property owners operational oversight, tenants a single point of contact, and vendors structured job intake with tracked performance.
Multi-tenant isolation: Every business document is scoped to a propertyOwnerId.
Users gain access via two independent mechanisms:
- Membership — organizational role (building manager, vendor tech, tenant admin)
- UserUnit — personal location (apartment, parking, assigned workspace)
A single user can hold both (e.g., a building manager who also lives in the building).
2. Tech Stack
| Layer | Technology | Version |
|---|---|---|
| Language | TypeScript (strict mode) | ^5 |
| Framework | Next.js (App Router) | 16.1.4 |
| UI library | React | 19.2.3 |
| CSS | Tailwind CSS v4 | ^4 |
| Component kit | shadcn/ui | ^3.7.0 |
| Type-safe API | tRPC | 11.12.0 |
| State (client) | Zustand | 5.0.11 |
| Query cache | TanStack React Query | 5.90.21 |
| Authentication | Custom (Argon2id, HMAC, WebAuthn) | — |
| Validation | Zod | ^3.25.76 |
| AI | Anthropic Claude Opus 4 | claude-opus-4-6 |
| Icons | Lucide React | 0.544.0 |
| Date utils | date-fns + Luxon | 4.1.0 / 3.7.2 |
| i18n | next-intl | ^4.3.7 |
| ORM | Prisma | ^6.3.1 |
| Database | PostgreSQL | — |
| MCP | @modelcontextprotocol/sdk | 1.27.1 |
| Cloud (AWS) | S3, SES, SNS, Scheduler | ^3.978.0 |
| Passkeys | SimpleWebAuthn | ^13.2.2 |
| Password hash | argon2 | ^0.44.0 |
| TOTP | otplib | ^13.1.1 |
| Panels | react-resizable-panels | ^4.6.4 |
| Testing | Vitest + Testing Library | ^4.0.18 |
| Theming | next-themes | ^0.4.6 |
Important Auth Note: Ecopick uses a custom production-grade auth system. Do not
introduce NextAuth, Passport.js, or any auth library. Session cookies (ecopick_session
ecopick_refresh) are HMAC-signed and managed by the custom auth layer.
3. Project Structure
ecpk/
├── .claude/launch.json # Dev server config (port 4500)
├── package.json
├── tsconfig.json
├── next.config.ts
├── postcss.config.mjs
├── components.json # shadcn config
├── CLAUDE.md # Claude Code auto-loaded instructions
├── docs/
│ ├── ARCHITECTURE.md # This file
│ ├── ecopick-architecture.md # Earlier architecture reference
│ └── ecopick-operations.md # Reconciled operations spec
├── prisma/
│ └── schema.prisma # PostgreSQL schema (64+ models)
├── proxy.ts # Auth gate + i18n routing (replaces middleware.ts)
├── messages/ # i18n translation files (en, de, fr, it)
├── public/ # Static assets
├── app/
│ ├── layout.tsx # Root layout + providers
│ ├── (pages)/[locale]/ # Locale-prefixed page routes
│ │ ├── layout.tsx # Dashboard shell
│ │ ├── page.tsx # Dashboard home
│ │ └── tickets/
│ │ ├── page.tsx # Ticket dashboard (stats + cards)
│ │ └── [ticketId]/page.tsx # Ticket workspace (3-panel)
│ └── api/
│ ├── auth/ # 18 authentication endpoints
│ ├── tickets/ # 7 ticket REST endpoints
│ ├── admin/ # Approvals, audit, security monitoring
│ ├── vendors/ # Vendor inbox + dispatch response
│ ├── dev/seed/route.ts # Dev seed (non-production)
│ ├── trpc/[trpc]/route.ts # tRPC HTTP handler
│ ├── events/[propertyOwnerId]/route.ts # SSE endpoint
│ ├── ai/stream/route.ts # AI streaming endpoint
│ ├── mcp/route.ts # Model Context Protocol
│ └── ... # Memberships, invitations, etc.
└── src/
├── client/
│ ├── devSession.ts # Dev session (localStorage)
│ ├── api.ts # REST fetch wrapper
│ └── hooks/ # useDevSessionState, useTickets
├── components/
│ ├── tickets/workspace/ # 3-panel ticket workspace
│ │ ├── TicketWorkspace.tsx # Main workspace component
│ │ ├── TicketLeftPanel.tsx # Metadata panel
│ │ ├── TicketRightPanel.tsx # Operations panel
│ │ └── sections/ # 12 lazy-loaded section components
│ │ ├── SectionRenderer.tsx
│ │ ├── OverviewSection.tsx
│ │ ├── DetailsSection.tsx
│ │ ├── ActivitySection.tsx
│ │ ├── TeamSection.tsx
│ │ ├── HistorySection.tsx
│ │ ├── SLASection.tsx
│ │ ├── AttachmentsSection.tsx
│ │ ├── AISection.tsx
│ │ ├── RelatedSection.tsx
│ │ ├── BillingSection.tsx
│ │ └── VendorSection.tsx
│ └── ui/ # 42 shadcn/ui components
├── i18n/ # next-intl routing + navigation
├── lib/auth/ # Auth system (session, crypto, device, rate limit)
├── server/
│ ├── ai/
│ │ └── anthropicService.ts # Anthropic Claude integration
│ ├── auth/
│ │ └── appAdmin.ts # App admin checks
│ ├── db/
│ │ └── prisma.ts # Prisma client singleton
│ ├── errors.ts # AppError class
│ ├── governance/
│ │ ├── policies.ts # Policy-as-code (authorize())
│ │ ├── permissions.ts # ResourcePermission service
│ │ ├── auditGovernance.ts # Audit functions
│ │ ├── resourceActions.ts # Action + ResourceType definitions
│ │ └── index.ts # auditWrite, auditRead, diffFields
│ ├── routing/
│ │ └── routingService.ts # Auto-routing logic
│ ├── tickets/
│ │ └── ticketService.ts # Ticket business logic (623 lines)
│ └── trpc/
│ ├── init.ts # tRPC init + context + procedures
│ ├── root.ts # Root appRouter (24 sub-routers)
│ └── routers/ # 24 router files
├── store/
│ ├── ticketUIStore.ts # Active section, panel state (persisted)
│ ├── uiStore.ts # Command palette
│ ├── sessionStore.ts # Current user
│ └── notificationStore.ts # Toast stack
└── mcp/
└── server.ts # Model Context Protocol server
4. Data Models (Prisma Schema — 64+ models)
4.1 Organizational Hierarchy
PropertyOwner
Top-level business entity — property management companies or landlords. Every building belongs to exactly one PropertyOwner.
id, name, ownerType (property_management | landlord), createdAt, updatedAt
Relations: buildings[], memberships[], routingRules[], slaPlans[], tickets[],
vendorSubscriptions[], serviceCatalogs[], scheduledTasks[],
vendorPerformances[], invoicesReceived[], teamTemplates[], auditEvents[]
TenantCompany
Corporate tenant leasing space in a building (e.g., "Bloomberg LP").
id, name, companyType (corporate | residential | retail), createdAt, updatedAt
Relations: leases[], memberships[], tickets[]
Lease
Links TenantCompany to Building + specific units.
id, tenantCompanyId, buildingId, unitIds[], startDate, endDate?, isActive, createdAt, updatedAt
Indexes: tenantCompanyId, buildingId, isActive
Building
Physical building managed by a PropertyOwner.
id, propertyOwnerId, name, address1?, address2?, city?, postalCode?, country?,
createdAt, updatedAt
Relations: units[], userUnits[], tickets[], routingRules[], memberships[],
vendorSubscriptions[], scheduledTasks[], leases[]
Indexes: propertyOwnerId
Unit
Apartment, office, parking spot, or storage unit within a building.
id, buildingId, label, unitType (UnitType enum), floor?, description?, createdAt, updatedAt
Relations: userUnits[], tickets[], memberships[], sections[], routingRules[], scheduledTasks[]
Unique: [buildingId, label]
Indexes: buildingId, unitType
UnitSection
Specific area within a unit (kitchen, bathroom, corridor, etc.) for precise issue reporting.
id, unitId, name, sectionType (SectionType enum), floor?, roomNumber?,
description?, isActive, createdAt, updatedAt
Relations: tickets[]
Unique: [unitId, name]
Indexes: unitId, sectionType, isActive
4.2 Users & Access
User
Any person interacting with the platform. Dual-context access via Membership + UserUnit.
id, email (unique), emailVerified, name?, firstName?, lastName?, birthDate?, sex?,
language, isAppAdmin, passwordHash?, passwordUpdatedAt?, twoFactorEnabled,
totpSecretEnc?, twoFactorEnabledAt?, createdAt, updatedAt
Auth relations: authSessions[], authRefreshTokens[], authPendingLogins[],
authRecoveryCodes[], authAuditEvents[], webauthnCredentials[],
securityAlerts[], blockedSources[], rateLimitViolations[], rateLimitAttempts[]
Governance relations: accessEvents[], accessDecisions[], accessRequestsRequested[],
accessRequestsToDecide[], accessRequestTasksToReview[],
resourcePermissions[] (TO this user), grantedResourcePermissions[] (BY this user)
Business relations: memberships[], userUnits[], reportedTickets[], ticketStakeholders[],
messages[], uploadedAttachments[], auditEvents[], onboardings[], notifications[],
vendorOpportunityViews[], scheduledTasksAssigned[], invoicesApproved[],
aiInsightsDismissed[], aiInsightsFlagged[]
UserUnit
Personal relationship between User and Unit (resident, owner, parking).
id, userId, unitId, buildingId, relationship, isPrimary, canRaiseTickets,
startDate, endDate?, isActive, createdAt, updatedAt
Unique: [userId, unitId]
Indexes: userId, unitId, buildingId, isActive
Membership
Organizational role linking User to PropertyOwner, TenantCompany, or VendorOrg.
id, userId, role (MembershipRole enum), propertyOwnerId?, tenantCompanyId?,
vendorOrgId?, buildingId?, unitId?, createdAt, updatedAt
Indexes: userId, propertyOwnerId, tenantCompanyId, vendorOrgId, buildingId, unitId
MembershipRole Enum
Property Owner staff: PROPERTY_OWNER, BUILDING_MANAGER, BUILDING_MAINTENANCE
Tenant Company staff: TENANT_ADMIN, TENANT_COORDINATOR
Occupants: TENANT_OCCUPANT, RESIDENT
Legacy (compat): BUILDING_ADMIN, CARETAKER
Vendors: VENDOR_ADMIN, VENDOR_TECH
4.3 Tickets
Ticket
Core work request entity — the central object of the platform.
id, propertyOwnerId, tenantCompanyId?, buildingId, unitId?, unitSectionId?,
title, description?, category, priority (TicketPriority), status (TicketStatus),
reporterId, currentAssignmentId?, vendorOrgId?, slaDueAt?, slaBreachedAt?,
triagedAt?, resolvedAt?, closedAt?, cancelledAt?, openToVendors, vendorDeadline?,
createdAt, updatedAt
Relations: assignments[], attachments[], auditEvents[], messages[], slaEvents[],
stakeholders[], vendorOpportunityViews[], accessRequests[], aiInsights[],
vendorPerformances[], invoices[]
Indexes: [propertyOwnerId, status], [tenantCompanyId, status], buildingId,
unitId, unitSectionId, vendorOrgId, [openToVendors, category]
TicketStatus Enum
NEW, TRIAGED, ASSIGNED, VENDOR_OPPORTUNITY, VENDOR_PENDING,
IN_PROGRESS, AWAITING_TENANT, AWAITING_VENDOR, RESOLVED, CLOSED, CANCELLED
TicketPriority Enum
LOW, MEDIUM, HIGH, URGENT
Assignment
Links a Ticket to a person or vendor for resolution.
id, ticketId, type (AssignmentType), state (AssignmentState),
assignedToUserId?, vendorOrgId?, createdAt, updatedAt
Indexes: ticketId, vendorOrgId, assignedToUserId
AssignmentType Enum
INTERNAL, VENDOR
AssignmentState Enum
PENDING, ACCEPTED, DECLINED, CANCELLED
Important: No COMPLETED value. Do not use AssignmentState.COMPLETED.
TicketStakeholder
Observer/participant on a ticket with fine-grained permissions.
Prisma accessor: prisma.ticketStakeholder (not prisma.stakeholder).
id, ticketId, userId, role, canView, canComment, canEdit, canResolve,
addedById, addedAt, removedAt?
Unique: [ticketId, userId]
Indexes: ticketId, userId
Message
Comment or update on a ticket's activity feed.
Important: Content field is body (not content).
id, ticketId, authorId, body, createdAt, updatedAt
Relations: attachments[]
Indexes: [ticketId, createdAt]
Attachment
File uploaded to a ticket (comment attachment or resolution evidence).
id, ticketId, messageId?, uploadedById, type (AttachmentType),
storageKey, fileName, contentType?, sizeBytes?, createdAt
Indexes: ticketId, messageId, type
AttachmentType Enum
COMMENT_ATTACHMENT, RESOLUTION_EVIDENCE
4.4 Vendors
VendorOrg
External service provider organization.
id, name, createdAt, updatedAt
Relations: assignments[], memberships[], routingRules[], tickets[], subscriptions[],
opportunityViews[], serviceCatalogs[], scheduledTasks[],
vendorPerformances[], billingRates[], invoicesSent[]
VendorSubscription
Categories + buildings a vendor wants to receive opportunities for.
id, vendorOrgId, buildingId?, propertyOwnerId, categories[], isActive,
notifyUrgent, notifyHigh, notifyMedium, notifyLow,
maxConcurrentJobs?, serviceRadiusMiles?, createdAt, updatedAt
Unique: [vendorOrgId, buildingId, propertyOwnerId]
VendorOpportunityView
Tracks when a vendor views an open ticket opportunity.
id, ticketId, vendorOrgId, viewedAt, viewedById
Unique: [ticketId, vendorOrgId]
VendorPerformance
Service quality metrics per completed ticket.
id, vendorOrgId, propertyOwnerId, ticketId, responseTimeMinutes?,
resolutionTimeMinutes?, rating? (1-5), ratingComment?, slaMetDeadline?, createdAt
Unique: [vendorOrgId, ticketId]
4.5 Routing & SLA
RoutingRule
Auto-assignment configuration per category/building.
id, propertyOwnerId, buildingId, unitId?, category,
defaultAssigneeUserId?, defaultVendorOrgId?, priority (int), isActive,
createdAt, updatedAt
Indexes: [propertyOwnerId, buildingId], [buildingId, unitId], category
SLAPlan
Response/resolution time targets per property owner.
id, propertyOwnerId, name, defaultMinutes (default 1440), createdAt, updatedAt
SLAEvent
SLA lifecycle events (started, paused, breached, met).
id, ticketId, planId?, type, occurredAt
Indexes: ticketId, occurredAt
4.6 Authentication
AuthSession
Active login session. Created on successful authentication.
id, userId, rememberMe, deviceFingerprint?, persistentDeviceId?,
userAgent?, ip?, location?, createdAt, expiresAt, lastSeenAt
Indexes: userId, deviceFingerprint, persistentDeviceId, expiresAt
AuthRefreshToken
Hashed refresh token for session rotation (1:1 with AuthSession).
id, sessionId (unique), userId, tokenHash, deviceFingerprint?,
persistentDeviceId?, createdAt, expiresAt, lastUsedAt, revokedAt?
Indexes: userId, deviceFingerprint, persistentDeviceId, expiresAt, revokedAt
AuthPendingLogin
In-progress 2FA challenge (TOTP, email code, passkey).
id, userId, method, codeSalt?, codeHash?, codeAttempts, codeSentAt?,
createdAt, expiresAt, consumedAt?, ip?, deviceFingerprint?
Indexes: userId, expiresAt
AuthEmailVerification
Email verification during registration or email change.
id, email, salt, codeHash, attempts, createdAt, expiresAt, verifiedAt?, consumedAt?
Indexes: email, expiresAt, verifiedAt
AuthRecoveryCode
One-time backup codes for 2FA recovery.
id, userId, salt, hash, createdAt, usedAt?
Indexes: userId, usedAt
WebAuthnCredential
Registered passkey for passwordless login.
id, userId, credentialId (unique, Bytes), publicKey (Bytes), counter,
transports[], deviceFingerprint?, persistentDeviceId?, createdAt, lastUsedAt?
Indexes: userId, deviceFingerprint, persistentDeviceId, createdAt
4.7 Security Monitoring
AuthAuditEvent
Immutable log of auth events (login, logout, 2FA, password change).
id, userId?, email?, action, outcome, reasonCode?, ip?, userAgent?,
method?, route?, requestId?, correlationId?, deviceFingerprint?, meta?, createdAt
Indexes: createdAt, [userId, createdAt]
BlockedSource
Blocked IP, device fingerprint, or user account.
id, type (ip | device_fingerprint | user), value, reason?, blockedUntil?,
permanent, archived, unblockedAt?, meta?, userId?, createdAt, updatedAt
Unique: [type, value]
Indexes: [type, value], blockedUntil, archived, userId
RateLimitViolation
Rate limit threshold exceeded — aggregated violation + ban state.
id, userId?, key?, field?, type, method?, route?, ip?, deviceFingerprint?,
persistentDeviceId?, count, lastViolation, attempts?, bannedUntil?, expireAt?,
createdAt, updatedAt
Indexes: userId, deviceFingerprint, [type, field], bannedUntil, expireAt
RateLimitAttempt
Individual API request tracked for rate limiting (write-heavy, periodically pruned).
id, userId?, type, field?, key?, route?, method?, ip?, deviceFingerprint?,
persistentDeviceId?, userAgent?, createdAt
Indexes: userId, deviceFingerprint, createdAt
SecurityAlert
Automated threat detection (brute force, credential stuffing, impossible travel).
id, alertCode (unique), userId?, sourceId?, sourceType?, sourceSnapshot?,
riskScore (default 20), type, category, severity, status (open | resolved),
detectedAt, resolvedAt?, field?, key?, count, deviceInfo?, deviceFingerprint?,
persistentDeviceId?, ip?, method?, route?, meta?, createdAt, updatedAt
Indexes: userId, status, sourceId, deviceFingerprint, updatedAt
4.8 Governance
AccessEvent
Read audit log (every significant data access).
id, userId, action (AccessAction), resourceType, resourceId,
contextOrgId?, ip?, userAgent?, method?, route?, requestId?,
correlationId?, metadata?, createdAt
Indexes: [userId, createdAt], [resourceType, resourceId, createdAt], [contextOrgId, createdAt]
AccessDecision
Policy check audit trail (every authorize() call).
id, userId, action, resourceType, resourceId?, granted, policyName?,
reason?, ip?, userAgent?, method?, route?, requestId?, correlationId?, createdAt
Indexes: [userId, createdAt], [resourceType, resourceId, createdAt], [granted, createdAt]
AccessRequest
Formal request for elevated access or sensitive action.
id, ticketId?, requestedById, approverId, action, status (AccessRequestStatus),
reason?, decisionNote?, metadata?, submittedAt, decidedAt?, closedAt?,
expiresAt?, createdAt, updatedAt
Indexes: ticketId, [approverId, status], requestedById, [status, expiresAt]
AccessRequestTask
Individual review step in approval workflow.
id, accessRequestId, reviewerUserId, taskType, status (AccessRequestTaskStatus),
orderIndex, dueAt?, decidedAt?, decisionNote?, metadata?, createdAt, updatedAt
Indexes: [accessRequestId, status], [reviewerUserId, status], taskType
ResourcePermission
Granular per-resource permission grant (2D matrix).
id, userId, resourceType, resourceId, permissions (JSON matrix),
full, needsApproval, emergency, grantedById?, grantedAt, expiresAt?, revokedAt?,
createdAt, updatedAt
Unique: [userId, resourceType, resourceId]
Indexes: [resourceType, resourceId], userId, expiresAt, revokedAt
Permission matrix format: { "view": ["*"], "edit": ["units", "routing"], "comment": ["*"] }
- Keys = actions, Values = scopes (sub-resources / domains)
["*"]= unrestricted scope for that actionfull: true= god-mode bypassneedsApproval: true= requires approval workflowemergency: true= override normal restrictionsexpiresAt= temporary accessrevokedAt= soft revocation (record persists for audit)
4.9 Audit Event
AuditEvent
Immutable log of business mutations on tickets. Scoped to PropertyOwner.
id, propertyOwnerId, ticketId?, actorId?, action (AuditAction),
ip?, userAgent?, method?, route?, requestId?, correlationId?,
meta?, changes? (JSON [{field, from, to}]), createdAt
Indexes: [propertyOwnerId, createdAt], [ticketId, createdAt], [actorId, createdAt]
4.10 AI
AIInsight
AI-generated analysis persisted per ticket.
id, ticketId, type (triage_suggestion | vendor_recommendation | priority_assessment | summary),
content (JSON), confidence? (0.0-1.0), status (AIInsightStatus),
dismissedById?, flaggedById?, modelId?, promptVersion?, createdAt, updatedAt
Indexes: [ticketId, type], status, createdAt
AIInsightStatus Enum
GENERATED, ACCEPTED, DISMISSED, FLAGGED
4.11 Billing
BillingRate
Vendor service rate card.
id, vendorOrgId, category, rateType (HOURLY | FIXED | PER_UNIT),
amount (Decimal 10,2), currency (EUR), isActive, createdAt, updatedAt
Indexes: vendorOrgId, category, isActive
Note: Prisma Decimal serializes as string over JSON. Always wrap with Number().
Invoice
Vendor invoice to property owner.
id, vendorOrgId, propertyOwnerId, ticketId?, invoiceNumber (unique),
status (InvoiceStatus), totalAmount (Decimal 12,2), currency (EUR),
dueDate?, lineItems (JSON), approvedById?, paidAt?, createdAt, updatedAt
Indexes: vendorOrgId, propertyOwnerId, ticketId, status, dueDate
InvoiceStatus Enum
DRAFT, SUBMITTED, APPROVED, PAID, DISPUTED, CANCELLED
RateType Enum
HOURLY, FIXED, PER_UNIT
4.12 Supporting Models
ServiceCatalog + ServiceItem
Vendor/internal service offerings with estimated duration and category mapping.
ServiceCatalog: id, propertyOwnerId?, vendorOrgId?, name, description?, isActive
ServiceItem: id, catalogId, name, description?, category, estimatedDurationMinutes?,
requiresAccess, isActive
ScheduledTask
Recurring maintenance tasks using iCal RRULE with auto-ticket creation.
id, propertyOwnerId, buildingId, unitId?, title, description?, category,
rrule?, nextOccurrence?, assigneeUserId?, vendorOrgId?,
autoCreateTicket, lastTicketId?, isActive, createdAt, updatedAt
TeamTemplate
Reusable team composition templates with role requirements and permissions.
id, propertyOwnerId, name, description?, roles (JSON), isActive, createdAt, updatedAt
Notification + NotificationPreference
Multi-channel notifications (in_app, email, push) with per-user type preferences.
Notification: id, userId, type, title, body?, resourceType?, resourceId?,
channel (in_app | email | push), readAt?, sentAt?, createdAt
NotificationPreference: id, userId, type, channels[], enabled, updatedAt
Unique: [userId, type]
Invitation
Tokenized invite to join an org/building with a role.
id, token (unique), email, firstName?, lastName?, role, organizationType?,
organizationId?, buildingId?, unitId?, metadata?, status (pending | accepted | expired | revoked),
invitedBy?, createdAt, expiresAt, acceptedAt?, revokedAt?
UserOnboarding
Multi-step onboarding progress tracker (user setup + role-specific).
id, userId, type (user | role), role?, step, data?, startedAt, completedAt?, updatedAt
Unique: [userId, type]
4.13 Enums Summary
| Enum | Values |
|---|---|
| UnitType | APARTMENT, OFFICE, PARKING, STORAGE, RETAIL, COMMON_AREA, AMENITY, ROOFTOP, OTHER |
| SectionType | KITCHEN, PANTRY, BATHROOM, BEDROOM, LIVING_ROOM, DINING_ROOM, CORRIDOR, ENTRANCE, BALCONY, CLOSET, LAUNDRY, CONFERENCE_ROOM, WORKSPACE, RECEPTION, BREAKROOM, RESTROOM, LOBBY, STAIRWELL, ELEVATOR, MECHANICAL, UTILITY, OTHER |
| MembershipRole | PROPERTY_OWNER, BUILDING_MANAGER, BUILDING_MAINTENANCE, TENANT_ADMIN, TENANT_COORDINATOR, TENANT_OCCUPANT, RESIDENT, BUILDING_ADMIN, CARETAKER, VENDOR_ADMIN, VENDOR_TECH |
| TicketStatus | NEW, TRIAGED, ASSIGNED, VENDOR_OPPORTUNITY, VENDOR_PENDING, IN_PROGRESS, AWAITING_TENANT, AWAITING_VENDOR, RESOLVED, CLOSED, CANCELLED |
| TicketPriority | LOW, MEDIUM, HIGH, URGENT |
| AssignmentType | INTERNAL, VENDOR |
| AssignmentState | PENDING, ACCEPTED, DECLINED, CANCELLED |
| AttachmentType | COMMENT_ATTACHMENT, RESOLUTION_EVIDENCE |
| AccessAction | VIEW, LIST, SEARCH, EXPORT, DOWNLOAD |
| AccessRequestStatus | DRAFT, SUBMITTED, IN_REVIEW, APPROVED, REJECTED, CANCELLED, CLOSED, EXPIRED |
| AccessRequestTaskStatus | PENDING, IN_PROGRESS, APPROVED, REJECTED, SKIPPED, CANCELLED |
| AuditAction | TICKET_CREATED, STATUS_CHANGED, TRIAGED, ASSIGNED, DISPATCHED, VENDOR_ACCEPTED, VENDOR_DECLINED, MESSAGE_POSTED, ATTACHMENT_UPLOADED, RESOLVED, CLOSED, CANCELLED, STAKEHOLDER_ADDED, STAKEHOLDER_REMOVED |
| AIInsightStatus | GENERATED, ACCEPTED, DISMISSED, FLAGGED |
| InvoiceStatus | DRAFT, SUBMITTED, APPROVED, PAID, DISPUTED, CANCELLED |
| RateType | HOURLY, FIXED, PER_UNIT |
4.14 Data Deletion Philosophy
Operational data is never hard-deleted. All records transition through status-based lifecycles:
| Entity | Status Transitions |
|---|---|
| Ticket | NEW → TRIAGED → ASSIGNED → IN_PROGRESS → RESOLVED → CLOSED / CANCELLED |
| Assignment | PENDING → ACCEPTED / DECLINED / CANCELLED |
| Invoice | DRAFT → SUBMITTED → APPROVED → PAID / DISPUTED / CANCELLED |
| SecurityAlert | open → resolved |
| Invitation | pending → accepted / expired / revoked |
| AccessRequest | DRAFT → SUBMITTED → IN_REVIEW → APPROVED / REJECTED / EXPIRED / CANCELLED |
| TicketStakeholder | Active → removedAt set (soft remove) |
| ResourcePermission | Active → revokedAt set (soft revoke) |
| AuditEvent | Immutable — never deleted |
| AuthAuditEvent | Immutable — never deleted |
| AccessEvent | Immutable — never deleted |
5. Permission Model (Policy-as-Code)
5.1 Architecture
Single enforcement point: authorize() in src/server/governance/policies.ts.
Fail-closed design — unknown actions are denied. Every decision is audited.
authorize(action: string, ctx: PolicyContext): Promise<void>
// Throws AppError(403) if denied. Audits every decision (granted + denied).
// App admins bypass all checks.
5.2 Role Hierarchy
MANAGEMENT_ROLES: PROPERTY_OWNER, BUILDING_MANAGER, BUILDING_ADMIN
OPERATIONS_ROLES: MANAGEMENT_ROLES + BUILDING_MAINTENANCE + CARETAKER
VENDOR_ROLES: VENDOR_ADMIN, VENDOR_TECH
5.3 Policy Registry (5 domains)
ticket:view
- Explicit ResourcePermission grant
- Operations role membership in property owner
- Ticket reporter
- Resident of ticket's unit (via UserUnit)
- Member of assigned vendor org
- Active stakeholder with canView=true
ticket:edit
- Explicit ResourcePermission grant
- Management role membership
- Active stakeholder with canEdit=true
ticket:resolve
- Explicit ResourcePermission grant
- Management role membership
- Active stakeholder with canResolve=true
ticket:assign
- Explicit ResourcePermission grant
- Management role membership only
ticket:triage
- Explicit ResourcePermission grant
- Operations role membership
ticket:export
- Explicit ResourcePermission grant
- Management role membership only
ticket:create
- Explicit ResourcePermission (raise_tickets on unit)
- Operations role membership
- Resident of target unit (via UserUnit)
- Tenant role membership (TENANT_OCCUPANT, TENANT_ADMIN, TENANT_COORDINATOR)
building:view
- Explicit ResourcePermission grant
- Any membership in property owner
- Active UserUnit in building
building:manage
- Explicit ResourcePermission grant
- Management role membership only
vendor:view_opportunities / vendor:respond_assignment
- Explicit ResourcePermission grant
- Vendor role membership in vendor org
audit:view — Management only
audit:export — PROPERTY_OWNER only
approval:view — Management only
approval:create — Operations roles
approval:decide — Management only
5.4 ResourcePermission (Granular Overrides)
2D matrix: { action: scope[] }. Works alongside Membership/UserUnit.
Functions: hasPermission, hasAllPermissions, hasAnyPermission, getPermissions,
grantPermissions, setPermissions, revokePermissions, revokeScopes,
grantPermissionsBulk, getAccessibleResources, getAllPermissionsForUser.
6. Authentication System
6.1 Session Cookies
| Cookie | Purpose | TTL |
|---|---|---|
ecopick_session | HMAC-signed session ID | 12h (default) or 30d (rememberMe) |
ecopick_refresh | Stateful refresh token | 30d |
6.2 Session Lifecycle
- Login (password/passkey) →
issueSessionBundle()→ session + refresh token - Each request → verify signed cookie →
getSessionUser() - Session expired →
rotateSessionBundleFromRefreshToken()→ new session - Refresh token rotation: old token invalidated, new one issued
6.3 Security Layers
- HMAC-SHA256 cookie signing with timing-safe comparison
- Argon2id password hashing
- Device fingerprinting (non-invasive: UA + accept-language + platform)
- Progressive rate limiting per email/IP/device fingerprint
- Blocked source tracking (IP, device, user)
- AES-256-GCM encryption for sensitive data (TOTP secrets)
6.4 2FA Methods
- TOTP — Time-based one-time password (otplib)
- Email OTP — Code sent via SES
- WebAuthn/Passkey — Passwordless via SimpleWebAuthn
6.5 Dev Seed Auth
POST /api/dev/seed creates a real AuthSession and sets cookies for immediate dev access.
In browser, call fetch('/api/dev/seed', {method: 'POST'}) to get cookies set automatically,
then set localStorage ecopick_dev_session_v1 from the response for the tickets dashboard.
6.6 Proxy (Auth Gate)
proxy.ts uses Next.js 16's proxy API (replaces middleware.ts):
- Public paths:
/api/auth/*,/api/trpc/*,/api/dev/*,/_next/*,/favicon* - All other routes: verify session → redirect to login if no session
- i18n routing: locale-prefixed paths (
/en/tickets,/de/tickets)
7. API Layer (tRPC — 24 routers)
7.1 tRPC Setup
// src/server/trpc/init.ts
interface TRPCContext {
user: SessionUser | null;
}
export const protectedProcedure = t.procedure.use(/* validates session */);
7.2 Root Router
export const appRouter = router({
tickets,
ai,
approvals,
adminAudit,
security,
vendors,
routingRules,
memberships,
permissions,
invitations,
units,
users,
reports,
onboarding,
appCheck,
auditIngest,
public,
aiInsights,
serviceCatalog,
scheduledTasks,
vendorPerformance,
billing,
notifications,
teamTemplates,
});
7.3 Router Procedures
ticketsRouter
| Procedure | Type | Input | Description |
|---|---|---|---|
list | query | buildingId?, status? | List tickets for user's property owner |
detail | query | ticketId | Full ticket with all relations |
create | mutation | buildingId, title, description?, category, priority?, autoRoute? | Create ticket + auto-route |
update | mutation | ticketId, title?, description?, category?, priority? | Update ticket fields |
addMessage | mutation | ticketId, body | Post message on ticket |
addAttachment | mutation | ticketId, messageId?, type, storageKey, fileName, contentType?, sizeBytes? | Upload attachment |
assign | mutation | ticketId, type, assignedToUserId?, vendorOrgId? | Assign to staff/vendor |
triageTicket | mutation | ticketId | Mark ticket as triaged |
transitionStatus | mutation | ticketId, status | Change ticket state |
exportTickets | query | buildingId?, status?, format? | Bulk export |
listOrgMembers | query | — | Users in caller's org (for assignment picker) |
listVendorOrgs | query | — | Vendors linked to caller's buildings |
billingRouter
| Procedure | Type | Input | Description |
|---|---|---|---|
listRates | query | vendorOrgId? | View billing rates |
listInvoices | query | vendorOrgId?, status?, ticketId? | List invoices (with ticket filter) |
export | query | invoiceId | Download invoice |
vendorsRouter
| Procedure | Type | Input | Description |
|---|---|---|---|
inbox | query | — | View vendor ticket opportunities |
respond | mutation | ticketId, accept | Accept/decline assignment |
membershipsRouter
| Procedure | Type | Input | Description |
|---|---|---|---|
list | query | — | Get user's memberships |
delete | mutation | membershipId | Remove membership |
addBusinessRole | mutation | role, orgId, buildingId? | Request business role |
addResidence | mutation | unitId, relationship | Add unit access |
invitationsRouter
| Procedure | Type | Input | Description |
|---|---|---|---|
create | mutation | email, role, orgType?, orgId?, buildingId?, unitId? | Send invitation |
list | query | — | View pending invitations |
accept | mutation | token | Accept invitation |
resend | mutation | invitationId | Resend invitation |
onboardingRouter (~1100 lines)
Multi-step setup flows: initOnboarding, createPropertyOwner, createBuilding, createUnit, inviteUser, and many more procedures for the complete setup wizard.
usersRouter
| Procedure | Type | Input | Description |
|---|---|---|---|
myUnits | query | context?, buildingId?, unitType? | List units user has access to |
approvalsRouter
| Procedure | Type | Input | Description |
|---|---|---|---|
list | query | status? | Pending approvals |
create | mutation | ticketId?, action, approverId, reason? | Request approval |
decide | mutation | approvalId, decision, note? | Approve/reject |
cancel | mutation | approvalId | Withdraw request |
routingRulesRouter
| Procedure | Type | Input | Description |
|---|---|---|---|
list | query | buildingId? | View routing rules |
create | mutation | buildingId, category, vendorOrgId?, assigneeUserId?, priority? | Create rule |
update | mutation | ruleId, ... | Modify rule |
delete | mutation | ruleId | Remove rule |
aiRouter
| Procedure | Type | Input | Description |
|---|---|---|---|
complete | mutation | prompt (max 4000), systemPrompt? | Generate AI text completion |
aiInsightsRouter
| Procedure | Type | Input | Description |
|---|---|---|---|
listByTicket | query | ticketId | Get AI insights for ticket |
create | mutation | ticketId, type, content, confidence?, modelId? | Store insight |
updateStatus | mutation | insightId, status | Mark dismissed/flagged |
permissionsRouter
Grant, revoke, and query ResourcePermission records.
securityRouter
changePassword, enableTwoFactor, disableTwoFactor, listSessions, revokeSession.
notificationsRouter
list, markRead, updatePreferences.
adminAuditRouter
listAuditEvents, exportAuditData.
Other routers
scheduledTasksRouter, serviceCatalogRouter, teamTemplatesRouter, unitsRouter, reportsRouter, publicRouter, auditIngestRouter, appCheckRouter, vendorPerformanceRouter.
8. REST API Routes
8.1 Authentication (/api/auth/)
| Method | Path | Description |
|---|---|---|
| POST | /auth/login | Password login (returns session cookie) |
| POST | /auth/register | User signup |
| POST | /auth/register/send-code | Send email verification code |
| POST | /auth/register/verify-code | Validate email code |
| POST | /auth/logout | Clear session |
| POST | /auth/refresh | Rotate refresh token |
| GET | /auth/me | Current user info |
| POST | /auth/password/change | Change password |
| POST | /auth/password-reset/request | Initiate password reset |
| POST | /auth/security | Security settings |
| POST | /auth/verify-access-password | Verify for sensitive ops |
| POST | /auth/2fa/start | Begin 2FA challenge |
| POST | /auth/2fa/verify | Complete 2FA |
| GET | /auth/email/exists | Check email availability |
| POST | /auth/passkeys/register/options | WebAuthn registration options |
| POST | /auth/passkeys/register/verify | Complete passkey registration |
| POST | /auth/passkeys/login/options | WebAuthn login options |
| POST | /auth/passkeys/login/verify | Complete passkey login |
| GET | /auth/passkeys/available | List registered passkeys |
| DELETE | /auth/passkeys/[passkeyId] | Remove passkey |
| GET | /auth/sessions | List active sessions |
| DELETE | /auth/sessions/[sessionId] | Logout specific session |
8.2 Tickets (/api/tickets/)
| Method | Path | Description |
|---|---|---|
| GET | /tickets | List tickets with filters |
| POST | /tickets | Create ticket |
| GET | /tickets/[ticketId] | Get ticket detail |
| PUT | /tickets/[ticketId] | Update ticket |
| POST | /tickets/assign | Assign to vendor/staff |
| POST | /tickets/status | Change status |
| POST | /tickets/triage | Categorize ticket |
| POST | /tickets/message | Add message |
| POST | /tickets/attachment | Upload attachment |
8.3 Other REST Routes
| Method | Path | Description |
|---|---|---|
| GET/POST | /memberships | List/create memberships |
| POST | /memberships/add-business-role | Request role |
| POST | /memberships/add-residence | Add unit access |
| GET/POST | /invitations | List/send invitations |
| GET | /invitations/[token] | View invitation details |
| POST | /invitations/[token]/accept | Accept invitation |
| GET/PUT/DELETE | /routing-rules/[ruleId] | Manage routing rules |
| GET/POST | /routing-rules | List/create routing rules |
| GET | /units/[unitId] | Unit details |
| GET | /units/[unitId]/sections | Unit sections |
| GET | /users/me/units | User's units |
| GET/POST | /admin/approvals | Approval management |
| POST | /admin/approvals/[approvalId]/decide | Decide on approval |
| GET | /admin/audit | View audit logs |
| GET/POST | /admin/security_and_monitoring/security_alerts | Security alerts |
| GET | /admin/security_and_monitoring/security_alerts/[alertId] | Alert detail |
| POST | /admin/security_and_monitoring/security_alerts/[alertId]/severity/update | Update severity |
| POST | /admin/security_and_monitoring/security_alerts/[alertId]/status/update | Update status |
| GET | /admin/security_and_monitoring/rate_limits_violations | Rate limit violations |
| GET | /public/buildings/[buildingId]/requests | Public ticket form |
| POST | /onboarding | Onboarding flow |
| GET | /vendors/inbox | Vendor inbox |
| POST | /vendors/dispatch/respond | Respond to dispatch |
| GET | /app/check-block | Check if user is blocked |
| POST | /audit | Log access event |
| GET | /events/[propertyOwnerId] | SSE event stream |
| GET | /reports/overview | Dashboard summary |
| POST | /ai/stream | Stream AI completion |
| POST | /mcp | Model Context Protocol |
| GET | /permissions | Query permissions |
| POST | /dev/seed | Dev seed (non-production only) |
9. State Machines
9.1 Ticket Lifecycle
NEW → TRIAGED → ASSIGNED → IN_PROGRESS → RESOLVED → CLOSED
↓
VENDOR_OPPORTUNITY → VENDOR_PENDING
↓
AWAITING_VENDOR
↓ (any active state)
AWAITING_TENANT
↓ (any active state)
CANCELLED
- Resolution requires RESOLUTION_EVIDENCE attachment
- SLA due date computed at creation based on priority
9.2 Assignment Lifecycle
PENDING → ACCEPTED (vendor/staff confirmed)
→ DECLINED (vendor/staff refused → re-route)
→ CANCELLED (admin cancelled)
9.3 Invoice Lifecycle
DRAFT → SUBMITTED → APPROVED → PAID
→ DISPUTED
→ CANCELLED
9.4 Access Request Lifecycle
DRAFT → SUBMITTED → IN_REVIEW → APPROVED
→ REJECTED
→ EXPIRED
→ CANCELLED
9.5 SLA Priority Targets
| Priority | Response Target |
|---|---|
| URGENT | 60 minutes |
| HIGH | 4 hours (240 min) |
| MEDIUM | 24 hours (1440 min) |
| LOW | 3 days (4320 min) |
10. Ticket Service (Business Logic)
File: src/server/tickets/ticketService.ts (~623 lines)
createTicket
Validates input via Zod, computes SLA due date from priority, creates ticket,
audits with field-level diffs, auto-routes if autoRoute !== false.
listTickets
Filtered by propertyOwnerId, buildingId, status. Max 200, newest first.
getTicketDetail
Full ticket with all relations: building, unit, unitSection, reporter, assignments (with vendorOrg), stakeholders, messages (with attachments + author), attachments, auditEvents (with actor).
triageTicket
Only NEW tickets. Sets status=TRIAGED, triagedAt timestamp.
Audits with field diff { status: NEW → TRIAGED }.
assignTicket
Creates Assignment record. Updates ticket status to ASSIGNED. Validates not CLOSED/CANCELLED. Audits with metadata (assignee/vendor).
vendorRespondToAssignment
Accept/decline vendor dispatch. On decline: resets ticket to TRIAGED, triggers re-routing excluding the declined vendor.
addMessage
Creates message on ticket. Audits.
addAttachment
Creates attachment record. Audits.
transitionTicketStatus
Validates not CLOSED/CANCELLED. Requires RESOLUTION_EVIDENCE for RESOLVED. Sets corresponding timestamp (resolvedAt, closedAt, cancelledAt). Audits with field diff.
autoRouteAndDispatch
Queries RoutingRules by category/building/unit (highest priority first). If ticket is NEW, auto-triages. Assigns to default assignee (INTERNAL) or vendor (VENDOR). Supports exclusion lists for re-routing.
computeSlaBreachesNow
Finds overdue tickets (slaDueAt < now, not yet breached). Batch-marks slaBreachedAt timestamp.
11. Zustand Stores
ticketUIStore (persisted via localStorage)
activeSection: TicketSection;
// overview | details | activity | team | history | sla | attachments | ai | related | billing | vendor
leftCollapsed: boolean;
rightCollapsed: boolean;
uiStore
commandPaletteOpen: boolean;
sessionStore
user: { id, email, name, role } | null
isLoaded: boolean
notificationStore
notifications: Notification[]
12. Ticket Workspace (Component Architecture)
3-panel resizable layout using react-resizable-panels.
Left Panel (TicketLeftPanel)
Ticket metadata: info card, attributes, context (building/unit/section), stakeholders list.
Center Panel (SectionRenderer)
Active section content via React.lazy + Suspense. 11 sections:
| Section | Component | Key Features |
|---|---|---|
| overview | OverviewSection | Ticket summary, reporter info, timeline |
| details | DetailsSection | Editable title, description, category, priority, status |
| activity | ActivitySection | Message thread with composer |
| team | TeamSection | Reporter, stakeholders, assignments with assign dialog |
| history | HistorySection | Audit event timeline with actor names and field diffs |
| sla | SLASection | SLA tracking, breach status, time remaining |
| attachments | AttachmentsSection | File gallery with upload |
| ai | AISection | AI insights (triage suggestions, vendor recommendations) |
| related | RelatedSection | Other tickets in same building |
| billing | BillingSection | Invoice table for this ticket |
| vendor | VendorSection | Vendor info + performance stats |
Right Panel (TicketRightPanel)
Operations: next action suggestion, SLA window countdown, AI insights preview, quick note composer.
Section Navigation
Sections are navigated via ticketUIStore.setActiveSection() and rendered
by SectionRenderer using React.lazy.
13. AI Integration
13.1 Anthropic Service
File: src/server/ai/anthropicService.ts
const MODEL = "claude-opus-4-6";
createCompletion(prompt, systemPrompt?): Promise<string> // max_tokens: 1024
streamCompletion(prompt, systemPrompt?): AsyncIterable<string> // max_tokens: 2048
13.2 AI Insights Pipeline
- Insight generated (triage_suggestion, vendor_recommendation, priority_assessment, summary)
- Stored as AIInsight with confidence score, model ID, prompt version
- Displayed in AI section + right panel preview
- Users can accept, dismiss, or flag insights
13.3 Streaming Endpoint
POST /api/ai/stream — Server-sent events for real-time AI text generation.
14. Audit Architecture
Four independent audit streams with cross-stream correlation:
HTTP Request
│
├── Business mutation → AuditEvent (requestId, correlationId)
├── Auth event → AuthAuditEvent (requestId, correlationId)
├── Read access → AccessEvent (requestId, correlationId)
└── Policy decision → AccessDecision (requestId, correlationId)
Stream 1: AuditEvent (Business Mutations)
Every ticket lifecycle change: created, assigned, triaged, resolved, etc.
Includes field-level change diffs via diffFields().
Scoped to PropertyOwner for multi-tenant isolation.
Stream 2: AuthAuditEvent (Authentication)
Login, logout, password change, 2FA enable/disable, passkey registration, failed attempts. Includes IP, device fingerprint, user agent.
Stream 3: AccessEvent (Read Access)
Every significant data access (VIEW, LIST, SEARCH, EXPORT, DOWNLOAD). Tracks which org context the user was operating in.
Stream 4: AccessDecision (Policy Audit)
Every authorize() call logged with policy name, grant/deny result, and reason.
Audit Functions
auditWrite(event): Promise<void> // Business mutation audit
auditRead(event): Promise<void> // Read access audit
auditAccessDecision(event): Promise<void> // Policy decision audit
diffFields(before, after): FieldDiff[] // Field-level change detection
snapshotFields(record): Snapshot // Pre-mutation snapshot
15. i18n (Internationalization)
Configuration
- Library: next-intl v4
- Locales: en, de, fr, it
- Default: en
- Routing: Locale-prefixed paths (
/en/tickets,/de/tickets) - Files:
/messages/{locale}.json
Implementation
proxy.tshandles locale routing via Next.js 16 proxy APIsrc/i18n/contains routing and navigation configuration- Translation keys organized by feature area
16. Dev Seed
POST /api/dev/seed (non-production only):
- Creates PropertyOwner, VendorOrg, Building, 3 Units
- Creates 7 users across all major roles:
- Admin (app admin + property owner)
- Tenant admin
- Vendor admin
- Caretaker
- Building supervisor
- Reporter (resident)
- Occupant
- Creates Memberships across roles + UserUnit records
- Creates RoutingRules for auto-dispatch
- Creates 6 tickets at various lifecycle stages:
- With messages, attachments, AI insights
- Across different statuses and priorities
- Creates vendor performance records, invoices
- Issues AuthSession + sets
ecopick_sessionandecopick_refreshcookies
17. Configuration
| Setting | Value |
|---|---|
| Dev server port | 4500 |
| Dev command | npm run dev (→ next dev -p 4500) |
| Build command | npm run build (→ next build) |
| Start command | npm run start (→ next start -p 4500) |
| Typecheck | npm run typecheck (→ tsc --noEmit) |
| Test | npm run test (→ vitest) |
| Prisma generate | npm run prisma:generate |
| Prisma migrate | npm run prisma:migrate |
| Prisma studio | npm run prisma:studio |
| AI key | ANTHROPIC_API_KEY env var |
| DB URL | DATABASE_URL env var |
| tRPC endpoint | /api/trpc |
| SSE endpoint | /api/events/[propertyOwnerId] |
| Session cookie | ecopick_session (12h / 30d with rememberMe) |
| Refresh cookie | ecopick_refresh (30d) |
| CSS engine | Tailwind v4 via @tailwindcss/postcss |
18. Key Patterns & Gotchas
Model Name Gotchas
- Use
prisma.ticketStakeholder(notprisma.stakeholder) - Message field is
body(notcontent) AssignmentStateenum: PENDING, ACCEPTED, DECLINED, CANCELLED — no COMPLETEDTicketStatusincludes: VENDOR_OPPORTUNITY, VENDOR_PENDING, AWAITING_TENANT, AWAITING_VENDOR- Prisma
Decimalserializes asstringover JSON — always wrap withNumber()when formatting
Auth Patterns
// REST route with auth
export const GET = withAuth(async (req, {user, session}) => {
await authorize("ticket:view", {userId: user.id, resourceType: "Ticket", ...});
// handler
});
// tRPC procedure (protectedProcedure already validates session)
myProcedure: protectedProcedure
.input(z.object({ticketId: z.string()}))
.query(async ({ctx, input}) => {
// ctx.user available
}),
Data Patterns
- No hard deletes on operational data. Use status transitions (state machines).
- Audit everything. Every mutation goes through
auditWrite()→ AuditEvent. - Validate with Zod. No raw
anytypes. No unvalidated inputs. - Policy check first. Every protected handler calls
authorize()before business logic. - Dual-context aware. Always consider both Membership (org) and UserUnit (personal) when checking access.
File Naming
kebab-case.tsfor utility filesPascalCase.tsxfor React components- Router files named after domain:
ticketsRouter.ts,billingRouter.ts - Types and schemas colocated with server code
Event-Driven Updates
Every mutation that modifies operational data:
- Validates permission via
authorize() - Performs database operation
- Logs audit entry via
auditWrite() - (Future) Emits event for real-time updates via SSE
19. What This Platform Does NOT Do
- Does not replace property management ERP software
- Does not do procurement or contract management
- Does not manage physical access control systems or CCTV
- Does not build native mobile apps (responsive web covers the use case)
- Does not process payments (tracks invoices, doesn't collect money)
- Does not manage lease agreements (stores lease references for access control)
This document is auto-maintained. Last updated: 2026-03-10.