Terms Setup Architecture

Architecture and design decisions for the Terms Setup feature

Architecture Overview

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 │
└─────────────────────────────────────────┘

Layer Responsibilities

API Layer

Location: apps/zooly-app/app/api/terms-setup/

The API layer consists of Next.js route handlers that:

  • Handle HTTP requests and responses
  • Parse request bodies and query parameters
  • Validate input data
  • Authenticate users
  • Call service layer functions
  • Return standardized responses

Endpoints:

  • POST /api/terms-setup/getOrCreateIpTerms - Get or create terms
  • POST /api/terms-setup/approveIpTerms - Approve and update terms
  • DELETE /api/terms-setup/deleteIpTerms - Soft-delete terms
  • GET /api/terms-setup/listIpTerms - List all terms for an account

Service Layer

Location: packages/app/srv/src/terms-setup/

The service layer contains core business logic:

  • Core Functions:

    • getOrCreateIpTerms() - Retrieve or generate terms
    • generateIpTerms() - Create new terms with defaults
    • approveIpTerms() - Update and approve terms
    • deleteIpTerms() - Soft-delete terms
    • listIpTerms() - List all terms for an account
  • Shared Utilities:

    • formatIpTerms() - Format database records into human-readable legal text
    • resolveAccountId() - Handle authentication and authorization
    • Response helpers - Standardized success/error responses

Database Layer

Location: packages/db/src/access/ipTerms.ts

The database layer provides type-safe data access:

  • getIpTermsById() - Get term by ID
  • getIpTermsByAccountAndType() - Get term by account and type
  • listIpTermsByAccountId() - List all terms for account
  • createIpTerms() - Create new term
  • updateIpTerms() - Update existing term
  • softDeleteIpTerms() - Soft-delete term

Important: 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.

Package Structure

@zooly/terms Package

Location: packages/terms/

A pure TypeScript package (no dependencies on app-db or srv) that contains:

  • Legal Text Templates - Blueprints for each term type
  • Default Term Generators - Functions that create default term structures
  • Formatters - Functions that convert database records into formatted legal text

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.

Data Flow

Getting or Creating Terms

  1. Client Request → API route handler receives POST request with ipTermType
  2. Authentication → Route handler authenticates user and resolves account
  3. Service Call → Calls getOrCreateIpTerms() in service layer
  4. Database Query → Service calls getIpTermsByAccountAndType() to check for existing term
  5. Term Generation (if needed) → If not found, calls generateIpTerms() which:
    • Selects appropriate default function from @zooly/terms
    • Creates database record via createIpTerms()
  6. Formatting → Formats result using formatIpTerms() which:
    • Routes to type-specific formatter from @zooly/terms
    • Generates legal text with seller name
    • Converts compensation value to number
  7. Response → Returns formatted terms to client

Approving Terms

  1. Client Request → API route handler receives POST request with ipTermsId and update data
  2. Validation → Validates input (enum values, non-negative compensation, etc.)
  3. Authorization → Verifies user owns the term or has admin role
  4. Update → Service calls updateIpTerms() with:
    • Merged update data
    • ipApprove: true
    • updatedBy set to user ID
  5. Audit Logging → Logs approval event
  6. Formatting → Formats updated term
  7. Response → Returns formatted terms

Design Decisions

Why POST for Get-or-Create?

The getOrCreateIpTerms endpoint uses POST instead of GET because:

  • Side Effects: The endpoint creates records when they don't exist, which violates HTTP GET's requirement to be safe and idempotent
  • Explicit Intent: POST makes it clear that this operation may modify server state
  • Status Codes: Allows returning 201 Created for new terms vs 200 OK for existing ones

Soft Delete vs Hard Delete

Terms are soft-deleted (marked with deletedAt timestamp) rather than physically deleted because:

  • Audit Compliance: Preserves complete history for legal and compliance requirements
  • Dispute Resolution: Allows reviewing deleted terms if disputes arise
  • Recreation: Enables recreating terms of the same type after deletion (partial unique index only applies to active records)

Partial Unique Index

The database uses a partial unique index on [accountId, ipTermType] where deletedAt IS NULL:

  • Enforces Constraint: Only one active term per type per account
  • Allows Recreation: After soft-deleting, a new term of the same type can be created
  • Database-Level Enforcement: Prevents race conditions and ensures data integrity

Separation of Concerns

The architecture separates formatting logic (@zooly/terms) from business logic (@zooly/app-srv) and data access (@zooly/app-db):

  • Portability: Legal text templates can be reused in different contexts
  • Testability: Each layer can be tested independently
  • Maintainability: Changes to legal text don't require changes to business logic

Authentication & Authorization

Authentication Flow

  1. Route handler extracts cookie header from request
  2. Calls getVerifiedUserInfo() from @zooly/util which:
    • Validates session cookie
    • Calls auth server /api/me endpoint
    • Returns user object with roles array
  3. Fetches account via getAccountByUserId(user.id)

Authorization Rules

  • Own Account: Users can always manage terms for their own account
  • Admin Access: Users with "admin" role can manage terms for any account by providing accountId parameter
  • Ownership Verification: Before updates/deletes, system verifies term.accountId === user.accountId or user has admin role

Admin Role Check

When accountId parameter is provided and differs from user's account:

  1. Check user.roles?.includes("admin")
  2. If not admin, return 403 Forbidden
  3. If admin, proceed with operation on target account

Error Handling

All layers follow consistent error handling:

  1. Try-Catch Blocks - Wrap all logic
  2. Error Logging - Log errors with context
  3. Standardized Responses - Use errorResponse() helper with appropriate HTTP status codes
  4. Specific Messages - Return clear error messages for debugging

HTTP Status Codes:

  • 200 OK - Successful operation
  • 201 Created - Resource created (new term generated)
  • 400 Bad Request - Invalid input or validation error
  • 401 Unauthorized - Not authenticated
  • 403 Forbidden - Not authorized (admin check failed or ownership mismatch)
  • 404 Not Found - Resource not found
  • 500 Internal Server Error - Server error

Type Safety

The entire system uses strong TypeScript typing:

  • Database Schema - Drizzle ORM generates types from schema
  • Type Definitions - Shared types in @zooly/types package
  • Function Signatures - All functions have explicit parameter and return types
  • Enum Validation - Enum values validated at runtime and compile time

This ensures type safety across all layers and catches errors at compile time.

Future Enhancements

Client Components

The current implementation only covers the server-side API. Future work includes:

  • React components for managing terms in the UI
  • Form components for editing term fields
  • Display components for showing formatted legal text
  • Approval workflows in the UI

Audit Table

Create ip_terms_audit table to track:

  • Detailed change history
  • Previous and new values
  • Full audit trail for compliance

Terms Expiration

Add expiresAt field for time-limited terms that automatically expire.

Rate Limiting

Add rate limiting to API endpoints to prevent abuse.