Schema Design
This page documents the PostgreSQL schema powering Acctz Ledger -- its multi-tenant architecture, primary key strategy, Firebase Authentication integration, data integrity guarantees, and how to set it up from scratch.
Architecture Overview
The database is organized into four domain schemas inside a single PostgreSQL 17 instance. Each schema owns a bounded context and maps directly to the Architecture Plan domains.
| Schema | Purpose | Key Tables |
|---|---|---|
iam | Identity and access management | users, organizations, roles, role_permissions, org_memberships, api_tokens |
ledger | Core double-entry accounting | entities, accounts, account_types, account_templates, journal_entries, journal_lines, parties, fiscal_periods, attachments |
banking | Bank feeds, imports, reconciliation | bank_accounts, bank_transactions, import_jobs, reconciliations, reconciliation_items |
audit | Immutable compliance log | audit_log |
Multi-Tenant Design
Multi-tenancy follows a shared-database, shared-schema model with Row Level Security (RLS) enforcing isolation at the database level.
Tenant Hierarchy
Organization (org) -- billing unit, "the company"
└── Entity -- "company file" (a single set of books)
├── Accounts -- chart of accounts for this entity
├── Journal Entries -- financial transactions
├── Parties -- vendors, customers, employees
├── Fiscal Periods -- accounting periods
├── Bank Accounts -- linked bank feeds
└── Bank Transactions -- imported bank activity
An Organization is the top-level billing unit (equivalent to a QuickBooks "company"). Each Organization can have multiple Entities (sets of books). All tenant-scoped tables carry either org_id or entity_id as the isolation key.
Row Level Security
Every request from Spring Boot sets a session variable before touching the database:
SET LOCAL app.current_user_id = '<uuid>';
RLS policies on every tenant-scoped table use helper functions to restrict visibility:
-- Helper: which entities can the current user see?
CREATE FUNCTION ledger.accessible_entity_ids() RETURNS SETOF UUID AS $$
SELECT e.id FROM ledger.entities e
JOIN iam.org_memberships m ON m.org_id = e.org_id
WHERE m.user_id = current_setting('app.current_user_id', true)::UUID
AND m.status = 'active';
$$ LANGUAGE plpgsql STABLE SECURITY DEFINER;
-- Applied to every entity-scoped table:
ALTER TABLE ledger.accounts ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON ledger.accounts
USING (entity_id IN (SELECT ledger.accessible_entity_ids()));
This means tenant isolation is enforced at the database engine level, not just the application layer. Even raw SQL queries through pgAdmin or a JDBC connection will be scoped to the authenticated user's organizations.
Tables with RLS policies: iam.organizations, iam.org_memberships, iam.api_tokens, ledger.entities, ledger.accounts, ledger.fiscal_periods, ledger.parties, ledger.journal_entries, ledger.journal_lines, ledger.attachments, banking.bank_accounts, banking.bank_transactions, banking.import_jobs, banking.reconciliations, banking.reconciliation_items, audit.audit_log.