The Terms Setup feature follows a three-layer architecture pattern consistent with the Zooly monorepo structure:
┌─────────────────────────────────────────┐
│ API Layer (Next.js Route Handlers) │
│ apps/zooly-app/app/api/terms-setup/ │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ Service Layer (Business Logic) │
│ packages/app/srv/src/terms-setup/ │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ Database Layer (Data Access) │
│ packages/db/src/access/ipTerms.ts │
└─────────────────────────────────────────┘
Location: apps/zooly-app/app/api/terms-setup/
The API layer consists of Next.js route handlers that:
Endpoints:
POST /api/terms-setup/getOrCreateIpTerms - Get or create termsPOST /api/terms-setup/approveIpTerms - Approve and update termsDELETE /api/terms-setup/deleteIpTerms - Soft-delete termsGET /api/terms-setup/listIpTerms - List all terms for an accountLocation: packages/app/srv/src/terms-setup/
The service layer contains core business logic:
Core Functions:
getOrCreateIpTerms() - Retrieve or generate termsgenerateIpTerms() - Create new terms with defaultsapproveIpTerms() - Update and approve termsdeleteIpTerms() - Soft-delete termslistIpTerms() - List all terms for an accountShared Utilities:
formatIpTerms() - Format database records into human-readable legal textresolveAccountId() - Handle authentication and authorizationLocation: packages/db/src/access/ipTerms.ts
The database layer provides type-safe data access:
getIpTermsById() - Get term by IDgetIpTermsByAccountAndType() - Get term by account and typelistIpTermsByAccountId() - List all terms for accountcreateIpTerms() - Create new termupdateIpTerms() - Update existing termsoftDeleteIpTerms() - Soft-delete termImportant: The database layer never exposes raw table access. All database operations go through these access functions, ensuring consistent filtering of soft-deleted records and maintaining data integrity.
Location: packages/terms/
A pure TypeScript package (no dependencies on app-db or srv) that contains:
Structure:
packages/terms/
├── src/
│ ├── index.ts
│ └── enums/
│ └── ip-terms/
│ ├── voice-over/
│ │ ├── voice-over-terms.ts # Blueprint & defaults
│ │ └── formatIpTermsVoiceOver.ts # Formatter
│ ├── image/
│ └── likeness/
This package is designed to be portable and reusable across different contexts, not tied to any specific database or framework.
ipTermTypegetOrCreateIpTerms() in service layergetIpTermsByAccountAndType() to check for existing termgenerateIpTerms() which:
@zooly/termscreateIpTerms()formatIpTerms() which:
@zooly/termsipTermsId and update dataupdateIpTerms() with:
ipApprove: trueupdatedBy set to user IDThe getOrCreateIpTerms endpoint uses POST instead of GET because:
201 Created for new terms vs 200 OK for existing onesTerms are soft-deleted (marked with deletedAt timestamp) rather than physically deleted because:
The database uses a partial unique index on [accountId, ipTermType] where deletedAt IS NULL:
The architecture separates formatting logic (@zooly/terms) from business logic (@zooly/app-srv) and data access (@zooly/app-db):
getVerifiedUserInfo() from @zooly/util which:
/api/me endpointroles arraygetAccountByUserId(user.id)"admin" role can manage terms for any account by providing accountId parameterterm.accountId === user.accountId or user has admin roleWhen accountId parameter is provided and differs from user's account:
user.roles?.includes("admin")403 ForbiddenAll layers follow consistent error handling:
errorResponse() helper with appropriate HTTP status codesHTTP Status Codes:
200 OK - Successful operation201 Created - Resource created (new term generated)400 Bad Request - Invalid input or validation error401 Unauthorized - Not authenticated403 Forbidden - Not authorized (admin check failed or ownership mismatch)404 Not Found - Resource not found500 Internal Server Error - Server errorThe entire system uses strong TypeScript typing:
@zooly/types packageThis ensures type safety across all layers and catches errors at compile time.
The current implementation only covers the server-side API. Future work includes:
Create ip_terms_audit table to track:
Add expiresAt field for time-limited terms that automatically expire.
Add rate limiting to API endpoints to prevent abuse.
On This Page
Architecture OverviewLayer ResponsibilitiesAPI LayerService LayerDatabase LayerPackage Structure@zooly/terms PackageData FlowGetting or Creating TermsApproving TermsDesign DecisionsWhy POST for Get-or-Create?Soft Delete vs Hard DeletePartial Unique IndexSeparation of ConcernsAuthentication & AuthorizationAuthentication FlowAuthorization RulesAdmin Role CheckError HandlingType SafetyFuture EnhancementsClient ComponentsAudit TableTerms ExpirationRate Limiting