Monorepo Overview
Accountability uses a pnpm workspace monorepo with four main packages. Each package has a single responsibility and explicit dependencies.
accountability/
├── packages/
│ ├── core/ # Domain logic (Effect, 100% tested)
│ ├── persistence/ # Database layer (@effect/sql + PostgreSQL)
│ ├── api/ # HTTP API (Effect HttpApi + OpenAPI)
│ └── web/ # Frontend (React + TanStack Start)
├── specs/ # Specifications and documentation
├── repos/ # Reference repositories (git subtrees)
├── package.json # Workspace configuration
├── pnpm-workspace.yaml # pnpm workspace config
├── tsconfig.json # Shared TypeScript config
└── vitest.config.ts # Test configuration
Package Dependencies
web
│
├──→ api
│ │
│ ├──→ core
│ │ │
│ │ └──→ (no deps)
│ │
│ └──→ persistence
│ │
│ └──→ core
│
└──→ (openapi-fetch client generated from api)
The core package has no dependencies on other packages, ensuring the domain logic is completely independent of infrastructure concerns.
packages/core
Purpose: Pure domain logic, business rules, and service interfaces
Technology: Effect, Effect Schema
Key Characteristics:
- 100% test coverage required
- No external dependencies (only Effect)
- No database code
- No HTTP code
- Pure business logic
Structure
packages/core/src/
├── accounting/ # Chart of accounts domain
│ ├── Account.ts # Account entity
│ ├── AccountId.ts # Branded account ID
│ ├── AccountNumber.ts # Account number type
│ ├── AccountBalance.ts # Account balance calculations
│ ├── AccountHierarchy.ts # Account parent-child relationships
│ ├── AccountTemplate.ts # Standard chart of accounts templates
│ ├── AccountValidation.ts # Account validation rules
│ ├── BalanceValidation.ts # Trial balance validation
│ ├── TrialBalanceService.ts # Trial balance service
│ └── AccountErrors.ts # Domain errors
├── journal/ # Journal entries domain
│ ├── JournalEntry.ts
│ ├── JournalEntryLine.ts
│ ├── EntryStatusWorkflow.ts
│ ├── MultiCurrencyLineHandling.ts
│ ├── JournalEntryService.ts
│ └── JournalErrors.ts
├── fiscal/ # Fiscal periods domain
│ ├── FiscalYear.ts
│ ├── FiscalPeriod.ts
│ ├── FiscalPeriodStatus.ts
│ ├── FiscalPeriodType.ts
│ ├── FiscalPeriodService.ts
│ ├── YearEndCloseService.ts
│ └── FiscalPeriodErrors.ts
├── currency/ # Currency domain
│ ├── Currency.ts
│ ├── CurrencyCode.ts
│ ├── ExchangeRate.ts
│ ├── CurrencyService.ts
│ └── CurrencyErrors.ts
├── consolidation/ # Consolidation domain
│ ├── ConsolidationGroup.ts
│ ├── ConsolidationRun.ts
│ ├── EliminationRule.ts
│ ├── IntercompanyTransaction.ts
│ ├── ConsolidationMethodDetermination.ts
│ ├── ConsolidationService.ts
│ ├── EliminationService.ts
│ ├── IntercompanyService.ts
│ ├── NCIService.ts
│ └── ConsolidationErrors.ts
├── reporting/ # Financial reporting
│ ├── BalanceSheetService.ts
│ ├── IncomeStatementService.ts
│ ├── CashFlowStatementService.ts
│ ├── EquityStatementService.ts
│ ├── ConsolidatedReportService.ts
│ └── ReportErrors.ts
├── authentication/ # Authentication domain
│ ├── AuthUser.ts
│ ├── AuthUserId.ts
│ ├── AuthService.ts
│ ├── AuthProvider.ts
│ ├── PasswordHasher.ts
│ ├── Session.ts
│ ├── SessionId.ts
│ └── AuthErrors.ts
├── authorization/ # Authorization domain
│ ├── AuthorizationPolicy.ts
│ ├── PolicyEngine.ts
│ ├── AuthorizationService.ts
│ ├── PermissionMatrix.ts
│ ├── BaseRole.ts
│ ├── FunctionalRole.ts
│ └── AuthorizationErrors.ts
├── organization/ # Organization domain
│ ├── Organization.ts
│ └── OrganizationErrors.ts
├── company/ # Company domain
│ ├── Company.ts
│ ├── CompanyType.ts
│ └── CompanyErrors.ts
├── membership/ # Organization membership
│ ├── OrganizationMembership.ts
│ ├── OrganizationInvitation.ts
│ ├── OrganizationMemberService.ts
│ ├── InvitationService.ts
│ └── MembershipErrors.ts
├── audit/ # Audit logging
│ ├── AuditLog.ts
│ ├── AuditLogService.ts
│ └── AuditLogErrors.ts
├── jurisdiction/ # Jurisdictions
│ ├── Jurisdiction.ts
│ └── JurisdictionCode.ts
└── shared/ # Shared domain primitives
├── values/
│ ├── LocalDate.ts
│ ├── Timestamp.ts
│ ├── MonetaryAmount.ts
│ ├── Percentage.ts
│ └── Address.ts
├── context/
│ └── CurrentUserId.ts
└── errors/
├── SharedErrors.ts
└── RepositoryError.ts
Export Strategy
No barrel files - Each module exported explicitly in package.json:
{
"exports": {
"./accounting/Account": "./src/accounting/Account.ts",
"./accounting/AccountId": "./src/accounting/AccountId.ts",
"./journal/JournalEntry": "./src/journal/JournalEntry.ts"
}
}
Imports:
import { Account, AccountType } from "@accountability/core/accounting/Account"
import { AccountId } from "@accountability/core/accounting/AccountId"
import { JournalEntry } from "@accountability/core/journal/JournalEntry"
Naming Conventions
- Entities: PascalCase (Account, JournalEntry, Company)
- IDs: EntityId (AccountId, CompanyId, JournalEntryId)
- Services: EntityService (AccountService, JournalEntryService)
- Errors: EntityError or EntityNotFound (AccountNotFound, ValidationError)
packages/persistence
Purpose: Database persistence layer with repositories
Technology: @effect/sql, @effect/sql-pg, PostgreSQL
Key Characteristics:
- Repository pattern for data access
- SQL queries with Schema validation
- Database migrations
- Transaction support
Structure
packages/persistence/src/
├── Services/ # Repository interfaces
│ ├── AccountRepository.ts
│ ├── JournalEntryRepository.ts
│ ├── JournalEntryLineRepository.ts
│ ├── CompanyRepository.ts
│ ├── OrganizationRepository.ts
│ ├── FiscalPeriodRepository.ts
│ ├── ExchangeRateRepository.ts
│ ├── ConsolidationRepository.ts
│ ├── IntercompanyTransactionRepository.ts
│ ├── EliminationRuleRepository.ts
│ ├── UserRepository.ts
│ ├── SessionRepository.ts
│ ├── IdentityRepository.ts
│ ├── OrganizationMemberRepository.ts
│ ├── InvitationRepository.ts
│ ├── PolicyRepository.ts
│ ├── AuthorizationAuditRepository.ts
│ ├── AuditLogRepository.ts
│ ├── AuthService.ts # Auth service implementation
│ ├── LocalAuthProvider.ts # Local auth provider
│ ├── GoogleAuthProvider.ts # Google OAuth provider
│ └── WorkOSAuthProvider.ts # WorkOS SSO provider
├── Layers/ # Layer implementations
│ ├── AccountRepositoryLive.ts
│ ├── JournalEntryRepositoryLive.ts
│ ├── CompanyRepositoryLive.ts
│ ├── OrganizationRepositoryLive.ts
│ ├── FiscalPeriodRepositoryLive.ts
│ ├── AuthServiceLive.ts
│ ├── PolicyEngineLive.ts
│ ├── RepositoriesLive.ts # Composed layer with all repos
│ ├── MigrationsLive.ts # Migration runner
│ └── PgClientLive.ts # PostgreSQL client
├── Migrations/ # Database migrations
│ ├── Migration0001_CreateOrganizations.ts
│ ├── Migration0002_CreateCompanies.ts
│ ├── Migration0003_CreateAccounts.ts
│ ├── Migration0004_CreateFiscalPeriods.ts
│ ├── Migration0005_CreateJournalEntries.ts
│ ├── Migration0006_CreateExchangeRates.ts
│ ├── Migration0007_CreateConsolidation.ts
│ ├── Migration0008_CreateIntercompany.ts
│ ├── Migration0009_CreateConsolidationRuns.ts
│ ├── Migration0010_CreateAuthUsers.ts
│ ├── Migration0011_CreateAuthIdentities.ts
│ ├── Migration0012_CreateAuthSessions.ts
│ ├── Migration0013_CreateAuditLog.ts
│ └── Migration0017_CreateAuthorization.ts
├── Seeds/ # Seed data
│ └── SystemPolicies.ts # Default authorization policies
└── Errors/
└── RepositoryError.ts # Persistence errors
Repository Pattern
export interface AccountRepository {
readonly findById: (
organizationId: OrganizationId,
id: AccountId
) => Effect.Effect<Option.Option<Account>, PersistenceError>
readonly findByCompany: (
organizationId: OrganizationId,
companyId: CompanyId
) => Effect.Effect<ReadonlyArray<Account>, PersistenceError>
readonly create: (
account: Account
) => Effect.Effect<Account, PersistenceError>
}
export class AccountRepository extends Context.Tag("AccountRepository")<
AccountRepository,
AccountRepository
>() {}
packages/api
Purpose: HTTP API layer with OpenAPI spec generation
Technology: Effect HttpApi, HttpApiBuilder
Key Characteristics:
- Type-safe HTTP endpoints
- Automatic OpenAPI generation
- Middleware for auth/logging
- Error mapping to HTTP status codes
Structure
packages/api/src/
├── Definitions/ # API definitions
│ ├── AppApi.ts # Root API composition
│ ├── AuthApi.ts # Authentication endpoints
│ ├── AccountsApi.ts # Accounts endpoints
│ ├── CompaniesApi.ts # Companies endpoints
│ ├── JournalEntriesApi.ts # Journal entries endpoints
│ ├── FiscalPeriodsApi.ts # Fiscal periods endpoints
│ ├── CurrencyApi.ts # Currency endpoints
│ ├── ConsolidationApi.ts # Consolidation endpoints
│ ├── EliminationRulesApi.ts # Elimination rules endpoints
│ ├── IntercompanyTransactionsApi.ts # Intercompany endpoints
│ ├── ReportsApi.ts # Reporting endpoints
│ ├── AuthMiddleware.ts # Auth middleware
│ └── ApiErrors.ts # API-level errors
└── Layers/ # API implementations
├── AppApiLive.ts # Full API composition
├── AuthApiLive.ts
├── AccountsApiLive.ts
├── CompaniesApiLive.ts
├── JournalEntriesApiLive.ts
├── FiscalPeriodsApiLive.ts
├── CurrencyApiLive.ts
├── ConsolidationApiLive.ts
├── EliminationRulesApiLive.ts
├── IntercompanyTransactionsApiLive.ts
├── ReportsApiLive.ts
├── AuthMiddlewareLive.ts
└── OrganizationContextMiddlewareLive.ts
API Definition Pattern
// Definitions/AccountsApi.ts
const findById = HttpApiEndpoint.get("findById", "/accounts/:id")
.setPath(Schema.Struct({ id: AccountId }))
.addSuccess(Account)
.addError(NotFoundError, { status: 404 })
class AccountsApi extends HttpApiGroup.make("accounts")
.add(findById)
.add(create)
.add(update)
.prefix("/api/v1")
.middleware(AuthMiddleware)
{}
// Layers/AccountsApiLive.ts
export const AccountsApiLive = HttpApiBuilder.group(
AppApi,
"accounts",
(handlers) => handlers.handle("findById", ({ path }) =>
Effect.gen(function* () {
const service = yield* AccountService
return yield* service.findById(path.id)
})
)
)
packages/web
Purpose: React frontend with SSR
Technology: React, TanStack Start, openapi-fetch, Tailwind CSS
Key Characteristics:
- No Effect code (pure React)
- File-based routing
- SSR with loaders
- Type-safe API client
Structure
packages/web/src/
├── routes/ # File-based routes
│ ├── __root.tsx # Root layout
│ ├── index.tsx # Home page
│ ├── login.tsx # Login page
│ ├── register.tsx # Registration page
│ └── organizations/ # Organization routes
│ ├── index.tsx # List organizations
│ ├── new.tsx # Create organization
│ └── $organizationId/ # Nested routes
│ ├── route.tsx # Organization layout
│ ├── dashboard.tsx # Dashboard
│ ├── companies/
│ │ ├── index.tsx # List companies
│ │ ├── new.tsx # Create company
│ │ └── $companyId/ # Company routes
│ │ ├── index.tsx
│ │ ├── accounts/
│ │ ├── journal-entries/
│ │ ├── fiscal-periods/
│ │ └── reports/
│ ├── consolidation/
│ ├── intercompany/
│ ├── exchange-rates/
│ ├── audit-log/
│ └── settings/
├── components/ # Reusable components
│ ├── ui/ # Base UI components
│ │ ├── Button.tsx
│ │ ├── Input.tsx
│ │ ├── Select.tsx
│ │ ├── Dialog.tsx
│ │ ├── Table.tsx
│ │ └── Badge.tsx
│ ├── layout/ # Layout components
│ │ ├── AppLayout.tsx
│ │ ├── Sidebar.tsx
│ │ ├── Header.tsx
│ │ └── Breadcrumbs.tsx
│ ├── accounts/ # Account-specific components
│ ├── journal-entries/ # Journal entry components
│ └── reports/ # Report components
├── api/ # API client
│ ├── client.ts # openapi-fetch client
│ └── generated/ # Generated types
│ └── api-schema.ts # OpenAPI TypeScript types
├── utils/ # Utility functions
│ ├── format.ts # Formatting helpers
│ ├── validation.ts # Validation helpers
│ └── date.ts # Date utilities
├── hooks/ # Custom React hooks
│ ├── useDebounce.ts
│ └── useLocalStorage.ts
└── styles/
└── index.css # Global styles + Tailwind
Route Pattern
// routes/organizations/$organizationId/companies/index.tsx
export const Route = createFileRoute(
"/organizations/$organizationId/companies/"
)({
loader: async ({ request, params }) => {
const cookie = request.headers.get("cookie")
const { data } = await api.GET("/api/v1/companies", {
params: { query: { organizationId: params.organizationId } },
headers: cookie ? { cookie } : undefined
})
return { companies: data ?? [] }
},
component: CompaniesPage
})
function CompaniesPage() {
const { companies } = Route.useLoaderData()
const router = useRouter()
return (
<div>
<Header title="Companies" />
{companies.length === 0 ? (
<EmptyState
title="No companies yet"
action={<Button onClick={() => router.navigate({ to: "./new" })}>Create Company</Button>}
/>
) : (
<CompanyList companies={companies} />
)}
</div>
)
}
specs/
Purpose: Specifications, guides, and architectural documentation
specs/
├── guides/ # Consolidated guides
│ ├── effect-guide.md # Effect patterns
│ ├── testing-guide.md # Testing strategy
│ ├── frontend-guide.md # Frontend patterns
│ └── api-guide.md # API patterns
├── architecture/ # Architecture specs
│ ├── accounting-research.md # Domain model
│ ├── authentication.md # Auth system
│ ├── authorization.md # RBAC/ABAC
│ ├── error-design.md # Error handling
│ └── fiscal-periods.md # Fiscal periods
├── pending/ # Pending features
├── completed/ # Completed features
└── reference/ # Reference docs
Root Configuration Files
package.json
{
"name": "accountability",
"private": true,
"packageManager": "[email protected]",
"scripts": {
"dev": "pnpm --filter @accountability/web run dev",
"build": "pnpm --filter @accountability/web run build",
"test": "vitest run",
"typecheck": "tsc -b tsconfig.json"
}
}
pnpm-workspace.yaml
tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"skipLibCheck": true
}
}
Development Workflow
Adding a New Feature
- Core: Define entity in
packages/core/src/{domain}/
- Persistence: Add repository in
packages/persistence/src/Services/
- Persistence: Add migration in
packages/persistence/src/Migrations/
- API: Add endpoint in
packages/api/src/Definitions/
- API: Implement handler in
packages/api/src/Layers/
- Web: Regenerate API client:
pnpm --filter @accountability/web generate:api
- Web: Add route/component in
packages/web/src/routes/
- Test: Add tests at each layer
File Naming Conventions
- PascalCase: Component files, entity files (Account.ts, Button.tsx)
- camelCase: Utility files, hook files (format.ts, useDebounce.ts)
- kebab-case: Route files ($organization-id/companies/index.tsx)
- UPPER_CASE: Constants, environment variables (API_BASE_URL)
Next Steps