Database Schema
Bilko Database Schema
Status: IMPLEMENTED
(PrismaLastschemaverified:exists)2026-05-20Location:Canonical backend:Kotlin/Ktor service Database: PostgreSQL/Users/makinja/ALAI/products/Bilko/packages/database/prisma/schema.prismaapps/api14+on GCP Cloud SQL for deployed environmentsORM:Schema source of truth:PrismaFlyway5.xSQLLastmigrationsupdated:+2026-02-20Exposed table mappings
Purpose
This document describes the complete database schema forcurrently Bilko.used by the Bilko Kotlin/Ktor API.
It replaces the older ORM-era database notes and must be kept aligned with:
- Flyway migrations:
apps/api/src/main/resources/db/migration/ - Exposed table mappings:
apps/api/src/main/kotlin/no/alai/bilko/models/Tables.kt - Route/service behaviour:
apps/api/src/main/kotlin/no/alai/bilko/routes/andapps/api/src/main/kotlin/no/alai/bilko/services/ - Environment mapping:
infrastructure/gcp/ENV-MATRIX.md
Do not treat generated diagrams, frontend type definitions, or archived deployment notes as database authority.
1. Architecture Overview
Bilko is a multi-tenant accounting SaaS. The active backend stores tenant data in PostgreSQL and scopes business tables by organization_id where appropriate.
Runtime stack:
- API: Kotlin/Ktor
- SQL migration engine: Flyway
- Kotlin SQL mapping: JetBrains Exposed
- Database engine: PostgreSQL
- Deployed DB platform: GCP Cloud SQL
- Primary migration command path: Cloud Build / backend Gradle Flyway tasks
The schema is IMPLEMENTEDforward-only. inApplied Prismamigrations must not be edited after deployment. If a deployed environment has Flyway metadata drift, repair is handled as a controlled operations procedure with target identity checks, schema checks, transcript, and readypostflight for migration. This doc explains the relationships, constraints, and design decisions.validation.
Entity Relationship Overview
Organization (1) ──┬── (N) User
├── (N) Account
├── (N) Contact
├── (N) Invoice
├── (N) Expense
├── (N) Transaction
└── (N) BankAccount
Contact (1) ────┬── (N) Invoice
└── (N) Expense
Invoice (1) ──── (N) InvoiceItem
Account (1) ───┬── (N) InvoiceItem
├── (N) Expense
├── (N) BankAccount
├── (N) Transaction (debit)
├── (N) Transaction (credit)
└── (N) Account (parent-child hierarchy)
BankAccount (1) ── (N) BankTransaction
Currency (1) ───┬── (N) ExchangeRate (base)
└── (N) ExchangeRate (target)
User (1) ───┬── (N) Invoice (creator)
├── (N) Expense (creator)
├── (N) Expense (approver)
├── (N) Transaction (creator)
└── (N) LoggedAction
Core Tables
1. Organization
Purpose: Multi-tenant root. Every business is one organization.
Indexes:
Primary key:id
Business rules:
baseCurrency determines default currency for all transactionscountry determines tax rules (Serbia 20%, BiH 17%, Croatia 25%)fiscalYearStart used for annual reports
2. User
Source-of-Truth Purpose: Users within an organization. Role-based access control.
Indexes:
Primary key:idUnique:emailForeign key:organizationId→ Organization(id)Index:idx_users_organizationon organizationIdIndex:idx_users_emailon email
Enums:
enum UserRole {
owner // Full access, can delete org
admin // Can manage users and settings
accountant // Can create invoices/expenses
viewer // Read-only access
}
Business rules:
One owner per organization (enforced in API)Cannot delete ownerPassword must be bcrypt hashed, NEVER plain text
Chart of AccountsRules
3. AccountType
Purpose: Defines account categories for double-entry bookkeeping.
Enums:
enum NormalBalance {
debit // Asset, Expense accounts increase with debits
credit // Liability, Equity, Revenue accounts increase with credits
}
Seed data:
1 | Asset | debit
2 | Liability | credit
3 | Equity | credit
4 | Revenue | credit
5 | Expense | debit
4. Account
Purpose: Chart of Accounts. Hierarchical GL accounts.
Indexes:
Primary key:idUnique:(organizationId, code)Index:idx_accounts_organizationon organizationIdIndex:idx_accounts_typeon accountTypeId
Business rules:
Code MUST be unique within organizationCannot delete account with transactionsParent-child hierarchy for sub-accounts (e.g., 1000 → 1001, 1002)
Contacts (Customers & Vendors)
5. Contact
Purpose: Customers (invoice recipients) and vendors (expense payees).
Indexes:
Primary key:idIndex:idx_contacts_organizationon organizationIdIndex:idx_contacts_typeon type
Enums:
enum ContactType {
customer // Invoice recipient
vendor // Expense payee
both // Can be both customer and vendor
}
Business rules:
Soft delete (isActive = false) if has invoices/expensescurrencyCode determines default invoice/expense currency
Invoicing
6. Invoice
Purpose: Sales invoices (outgoing). Revenue recognition.
Indexes:
Primary key:idUnique:(organizationId, invoiceNumber)Index:idx_invoices_organizationon organizationIdIndex:idx_invoices_customeron customerIdIndex:idx_invoices_statuson statusIndex:idx_invoices_due_dateon dueDateComposite:idx_invoices_org_status_dateon (organizationId, status, invoiceDate)
Enums:
enum InvoiceStatus {
draft // Being edited
sent // Sent to customer
viewed // Customer viewed email
paid // Payment received
overdue // Past dueDate and unpaid
cancelled // Voided
}
Business rules:
invoiceNumber auto-generated on first saveexchangeRate locked at invoiceDate (NEVER recalculate)baseAmount = totalAmount * exchangeRateCannot edit invoice unless status = draftWhen status changes to 'paid', create Transaction (debit BankAccount, credit AccountsReceivable)
7. InvoiceItem
Purpose: Line items on invoices.
Indexes:
Primary key:idIndex:idx_invoice_items_invoiceon invoiceId
Business rules:
lineTotal = quantity * unitPrice (calculated before save)Tax amount = lineTotal * (taxRate / 100)accountId determines which revenue account is credited
Expenses
8. Expense
Purpose: Purchase tracking (incoming). Expense recognition.
Indexes:
Primary key:idUnique:(organizationId, expenseNumber)Index:idx_expenses_organizationon organizationIdIndex:idx_expenses_vendoron vendorIdIndex:idx_expenses_categoryon categoryIndex:idx_expenses_dateon expenseDateComposite:idx_expenses_org_date_categoryon (organizationId, expenseDate, category)
Enums:
enum ExpenseStatus {
pending // Awaiting approval
approved // Approved, ready to pay
paid // Payment made
rejected // Approval denied
}
Business rules:
expenseNumber auto-generatedexchangeRate locked at expenseDatebaseAmount = amount * exchangeRateWhen status → approved, create Transaction (debit ExpenseAccount, credit AccountsPayable)When status → paid, create Transaction (debit AccountsPayable, credit BankAccount)
Transactions (Double-Entry Ledger)
9. Transaction
Purpose: General ledger transactions. Every financial event creates a transaction.
Indexes:
Primary key:idIndex:idx_transactions_organizationon organizationIdIndex:idx_transactions_dateon transactionDateIndex:idx_transactions_debiton debitAccountIdIndex:idx_transactions_crediton creditAccountIdIndex:idx_transactions_referenceon (referenceType, referenceId)Composite:idx_transactions_org_dateon (organizationId, transactionDate)
Business rules:
DEBITS = CREDITS— Every transaction has exactly one debit and one creditdebitAccountId ≠ creditAccountId (enforced in API)Cannot edit if locked = trueCannot delete if reconciled = trueexchangeRate locked at transactionDatebaseAmount = amount * exchangeRate
Common transaction patterns:
-
InvoiceAddcreatedschema(draftchanges→viasent):new Flyway migrations only.Debit:UseAccountstheReceivablenext(Asset)available version underapps/api/src/main/resources/db/migration/.Credit:NeverRevenuerewrite(Revenue)an already-applied migration in demo, staging, or production.
-
InvoiceUpdatepaid:Exposed mappings in the same change.Debit:Tables.ktBankshouldAccountmatch(Asset)the Credit:migratedAccountsdatabaseReceivablesurface(Asset)used by routes/services.
-
ExpenseUpdateapproved:API and docs together.Debit:IfExpenseaAccountschema(Expense)change Credit:altersAccountsrequest/responsePayableshapes,(Liability)updatedocs/backend/openapi.yamland relevant backend docs.
-
ExpenseValidatepaid:with Flyway before deploy promotion.Debit:AAccountsvalidPayabledeploy(Liability)target must pass Flyway validation before the API is considered healthy.
Credit:BankKeep
Accountenvironment(Asset)identity explicit.- Staging, demo, and production databases are separate targets. Migration or repair work must state which database is being touched.
Banking3. &Migration ReconciliationInventory
10.
As BankAccountof this verification pass, the repository contains 36 Flyway migration files. The deployed stage repair for MC #101509 validated the current Flyway version as 35 after applying pending migrations.
Purpose:Important Bankmigration account metadata.groups:
Indexes:
Primary key:idIndex:idx_bank_accounts_organizationon organizationId
Business rules:
accountId MUST be Asset type accountcurrentBalance updated when transactions createdSoft delete (isActive = false)
11. BankTransaction
Purpose: Bank statement imports. For reconciliation.
Indexes:
Primary key:idIndex:idx_bank_transactions_accounton bankAccountIdIndex:idx_bank_transactions_dateon transactionDate
Business rules:
Imported from CSV bank statementsMatched to GL transactions via reconciliation workflowreconciled = true when matched
Multi-Currency
12. Currency
Purpose: Supported currencies.
Seed data:
EUR | Euro | € | 2
RSD | Serbian Dinar | din. | 2
BAM | Bosnian Mark | KM | 2
HRK | Croatian Kuna | kn | 2
USD | US Dollar | $ | 2
13. ExchangeRate
Purpose: Historical exchange rates.
Indexes:
Primary key:idUnique:(baseCurrency, targetCurrency, effectiveDate)Index:idx_exchange_rates_dateon effectiveDateIndex:idx_exchange_rates_pairon (baseCurrency, targetCurrency)
Business rules:
Rates fetched daily from ECB or fixer.io APIWhen creating transaction, rate is locked at transaction dateIf no rate for exact date, use nearest available (warn in logs)
Audit Trail
14. LoggedAction
Purpose: Immutable audit log. Captures all INSERT/UPDATE/DELETE.
Indexes:
Primary key:eventIdIndex:idx_logged_actions_timestampon actionTimestampIndex:idx_logged_actions_tableon tableNameIndex:idx_logged_actions_useron userId
Enums:
enum AuditAction {
INSERT
UPDATE
DELETE
}
Business rules:
APPEND-ONLY— NEVER delete or update recordsTriggered via Prisma middleware (automatic)Used for: compliance, debugging, rollback simulation
Schema Version
15. SchemaVersion
Purpose: Migration tracking.
| Version | |||
Business rules:
Updated by Prisma migrationsUsed to verify schema version matches application version
Data Types & Precision
NUMERIC(19,4) for ALL Money
CRITICAL: NEVER use float, double, or JavaScript number for currency.
Precision: 19 digits total, 4 decimal placesRange: -999,999,999,999,999.9999 to +999,999,999,999,999.9999Prisma type:DecimalPostgreSQL type:NUMERIC(19,4)
Why:
JavaScriptnumberhas 53-bit precision (safe up to 2^53 - 1 = 9,007,199,254,740,991)Financial calculations require exact decimal precisionExample: 0.1 + 0.2 = 0.30000000000000004 (float error)
Usage in code:
import { Decimal } from '@prisma/client/runtime'
const amount = new Decimal('125000.0000')
const taxRate = new Decimal('0.20')
const taxAmount = amount.times(taxRate) // 25000.0000
Constraints Summary
Primary Keys
All tables use UUID primary keys (except AccountType uses INT auto-increment).
Foreign Keys
All foreign keys have onDelete: Cascade (deleting organization deletes all data).
Unique Constraints
User.emailAccount.(organizationId, code)Invoice.(organizationId, invoiceNumber)Expense.(organizationId, expenseNumber)ExchangeRate.(baseCurrency, targetCurrency, effectiveDate)
Check Constraints
(Enforced in API layer, not database):
amount > 0 for all financial amountsdueDate >= invoiceDate for invoicesdebitAccountId ≠ creditAccountId for transactions
Indexes Strategy
Query Patterns Optimized
List by organization + filter:(organizationId, status, date)composite index on invoices(organizationId, category, date)composite index on expenses
Foreign key lookups:All foreign keys have indexes
Daterangequeries:Dedicated indexes ontransactionDate,invoiceDate,expenseDate,dueDate
Reconciliation:Index on(referenceType, referenceId)for transaction lookups
Migration Commands
# Generate Prisma Client
npx prisma generate
# Create migration
npx prisma migrate dev --name migration_name
# Apply migrations (production)
npx prisma migrate deploy
# Reset database (dev only)
npx prisma migrate reset
# Seed initial data
npx prisma db seed
Seed Data
AccountType
INSERT INTO account_types (id, name, normal_balance) VALUES
(1, 'Asset', 'debit'),
(2, 'Liability', 'credit'),
(3, 'Equity', 'credit'),
(4, 'Revenue', 'credit'),
(5, 'Expense', 'debit');
Currency
INSERT INTO currencies (code, name, symbol, decimal_places) VALUES
('EUR', 'Euro', '€', 2),
('RSD', 'Serbian Dinar', 'din.', 2),
('BAM', 'Bosnian Mark', 'KM', 2),
('HRK', 'Croatian Kuna', 'kn', 2),
('USD', 'US Dollar', '$', 2);
Entity Relationship Diagram
erDiagram
ORGANIZATION ||--o{ USER : "members"
ORGANIZATION ||--o{ CONTACT : "contacts"
ORGANIZATION ||--o{ INVOICE : "invoices"
ORGANIZATION ||--o{ EXPENSE : "expenses"
ORGANIZATION ||--o{ TRANSACTION : "transactions"
ORGANIZATION ||--o{ BANK_ACCOUNT : "bank accounts"
ORGANIZATION ||--o{ ACCOUNT_TYPE : "account types"
ORGANIZATION ||--o{ ACCOUNT : "chart of accounts"
ORGANIZATION ||--o{ CURRENCY : "currencies"
ORGANIZATION ||--o{ LOGGED_ACTION : "audit log"
INVOICE ||--o{ INVOICE_ITEM : "line items"
INVOICE }o--|| CONTACT : "billed to"
INVOICE }o--|| USER : "created by"
EXPENSE }o--o| CONTACT : "vendor"
EXPENSE }o--|| ACCOUNT : "category account"
ACCOUNT_TYPE ||--o{ ACCOUNT : "classifies"
ACCOUNT ||--o{ TRANSACTION : "debited in"
ACCOUNT ||--o{ TRANSACTION : "credited in"
ACCOUNT |o--o| BANK_ACCOUNT : "linked GL"
BANK_ACCOUNT ||--o{ BANK_TRANSACTION : "transactions"
BANK_TRANSACTION }o--o| TRANSACTION : "reconciles"
CURRENCY ||--o{ EXCHANGE_RATE : "base rates"
LOGGED_ACTION }o--o| USER : "performed by"
Domain groupings:
Identity:Organization, UserFinancial Core:Account, AccountType, TransactionInvoicing:Invoice, InvoiceItem, ContactExpenses:ExpenseBanking:BankAccount, BankTransactionCompliance:LoggedAction, SchemaVersionCurrency:Currency, ExchangeRate
Migration Strategy
Prisma Migrate Workflow
Bilko uses Prisma Migrate for all schema changes. No manual SQL migrations.
Development workflow
# 1. Edit packages/database/prisma/schema.prisma
# 2. Generate and apply migration
npx prisma migrate dev --name describe_your_change
# 3. Regenerate Prisma Client
npx prisma generate
# 4. Seed if new lookup data needed
npx prisma db seed
Production deployment workflow
# Applied automatically during Railway deploy via package.json postinstall:
# "postinstall": "prisma migrate deploy && prisma generate"
# Manual production apply:
npx prisma migrate deploy
# Verify migration status:
npx prisma migrate status
Migration naming conventions
| | |
| | |
| | |
| | |
| |
Zero-Downtime Migration Patterns
For production migrations on live data:
| ||
|
Rule:Never drop columns in the same migration that removes their usage from code. Deploy code first (ignoring old column), then migrate.
Pre-Deploy Migration Checklist
Before deploying any migration to production:
prisma migrate statusshows no drift from devMigration tested on a staging DB with production data volumeRollback plan documented (additive migrations are safe; destructive need manual rollback SQL)LoggedAction.applicationNamedefault changed from"fiken-clone-api"to"bilko-api"(pending — fix before first production deploy)Backup taken before running destructive migrations
Known Pre-Deploy Fix Required
-- LoggedAction.applicationName has legacy default value in current schema
-- Run via: npx prisma db execute --file fix_applicationName.sql
ALTER TABLE "LoggedAction"
ALTER COLUMN "applicationName" SET DEFAULT 'bilko-api';
UPDATE "LoggedAction"
SET "applicationName" = 'bilko-api'
WHERE "applicationName" = 'fiken-clone-api';
Enhanced Index Design
Index Inventory
All indexes defined in packages/database/prisma/schema.prisma and verified against query patterns:
| Purpose | |||
|---|---|---|---|
V1-V6 |
| schema, ||
V7-V15 |
| tiers, ||
V16-V24 |
| RLS ||
V25-V29 |
| Adapter ||
V30-V35 |
| hardening, ||
| normalization, | password ||
| | ||
| | ||
| | ||
| | ||
| | ||
| | ||
| | ||
| | ||
| |
Operational note from MC #101509:
- Staging checksum drift was detected for V22, V25, V26, and V28.
- Schema checks proved checksum-only drift before repair.
- Controlled Flyway repair + migrate brought staging to version 35 and
flyway validatepassed. - The authoritative stage Cloud Build trigger then succeeded.
4. Tenant and Security Model
Most business data is scoped by organization_id and accessed through authenticated Ktor routes. The schema includes:
- Organization-level tenant boundary.
- User roles per organization.
- Platform-admin marker for controlled platform operations.
- Refresh-token session storage.
- Logged action/audit table.
- Row-level-security related migration work in the V17 and V30+ series.
Application code must set the correct organization/user context before querying tenant-scoped tables. Any new table containing tenant data should include organization_id unless it is intentionally global reference data.
5. Tables
The following table inventory is derived from Tables.kt and active migrations.
Index5.1 Designorganizations
Tenant root table.
Key fields:
idnameregistration_numbervat_number,vat_country,vat_registered,vat_ratefirm_typebase_currencycountry,languagefiscal_year_startlogo_urlsecurity_settings- subscription/trial fields:
plan_tier,quota_invoices_month,quota_contacts,quota_users,stripe_customer_id,stripe_subscription_id,trial_started_at,trial_ends_at - lifecycle fields:
created_at,updated_at,version,deleted_at
Notes:
countryis constrained by migration history and used by market-specific tax/e-invoice logic.- BA jurisdiction support was added in the V22/V23/V24 migration set.
5.2 users
Authenticated users belonging to organizations.
Key fields:
idorganization_idemailpassword_hashfull_nameroletwo_factor_enabled,two_factor_secret,two_factor_backup_codesnotification_preferenceslast_login_at- invite fields:
invite_token,invite_expires_at statusis_platform_admin- lifecycle fields:
created_at,updated_at,version,deleted_at
Notes:
- Password and 2FA behaviour is implemented in the Kotlin auth services.
is_platform_adminwas added by V26.
5.3 refresh_tokens
Session refresh-token storage.
Key fields:
iduser_idjtiexpires_atcreated_atversion
Notes:
- Used by auth-lifecycle and logout/revocation flows.
- Session listing/revocation routes are documented in OpenAPI.
5.4 account_types
Reference data for account classifications.
Key fields:
idnamenormal_balancecreated_atversion
5.5 accounts
Chart of accounts entries.
Key fields:
idorganization_idcodenameaccount_type_idcurrency_codeparent_account_idis_active- lifecycle fields:
created_at,updated_at,version,deleted_at
Notes:
- Market-specific chart additions exist for BA and RS.
- Account hierarchy is represented with
parent_account_id.
5.6 contacts
Customers and vendors.
Key fields:
idorganization_idtypenameemail,phone- registration and tax identifiers:
registration_number,vat_number,jmbg,jmbg_hash,oib,oib_hash - address fields:
address_line1,address_line2,city,postal_code,country currency_codepayment_termsnotesis_active- lifecycle fields:
created_at,updated_at,version,deleted_at
Notes:
- Sensitive personal/business identifiers are handled through the Kotlin service layer and migration-provided columns.
5.7 invoices
Sales invoices and e-invoice tracking.
Key fields:
idorganization_idcustomer_idinvoice_number- dates:
invoice_date,due_date,sent_at,viewed_at,paid_at - money fields:
currency_code,exchange_rate,subtotal,tax_amount,discount_amount,total_amount,base_amount statusnotes,terms,pdf_url- e-invoice fields:
is_reverse_charge,sef_id,sef_document_id,sef_status,sef_submitted_at,sef_accepted_at created_by- lifecycle fields:
created_at,updated_at,version,deleted_at
Notes:
- Invoice status enum support was added by V18.
- Serbia SEF integration fields are present on the invoice table.
5.8 invoice_items
Line items for invoices.
Key fields:
idinvoice_idline_numberdescriptionquantityunit_pricetax_ratevat_exemptline_totalaccount_idcreated_atversiondeleted_at
5.9 recurring_invoices
Recurring invoice templates/schedules.
Key fields:
idorganization_idcontact_idfrequencynext_issue_dateday_of_monthcurrency_codenotesis_activetemplate_datacreated_at,updated_at
5.10 expenses
Purchase/expense records.
Key fields:
idorganization_idvendor_idexpense_numberexpense_date- money fields:
currency_code,exchange_rate,amount,base_amount,tax_amount categorypayment_methodaccount_iddescriptionreceipt_urlstatus- approval/payment fields:
approved_by,approved_at,paid_at created_by- lifecycle fields:
created_at,updated_at,version,deleted_at
Notes:
- Expense status enum support was added by V19.
5.11 transactions
General ledger transactions.
Key fields:
idorganization_idtransaction_datedescriptiondebit_account_idcredit_account_id- money fields:
amount,currency_code,exchange_rate,base_amount - source reference:
reference_type,reference_id - lock/reconciliation fields:
locked,locked_at,reconciled,reconciled_at notescreated_bycreated_atversiondeleted_at
5.12 bank_accounts
Bank accounts linked to ledger accounts.
Key fields:
idorganization_idaccount_idbank_nameaccount_numberibancurrency_codecurrent_balanceis_active- lifecycle fields:
created_at,updated_at,version,deleted_at
5.13 bank_transactions
Imported or entered bank movements.
Key fields:
idbank_account_idtransaction_dateamountdescriptionreferencereconciledmatched_transaction_idcreated_atversiondeleted_at
5.14 currencies
Currency reference data.
Key fields:
codenamesymboldecimal_placesis_activecreated_atversion
5.15 exchange_rates
Foreign-exchange rates.
Key fields:
idbase_currencytarget_currencyrateeffective_datesourcelast_updatedversiondeleted_at
5.16 logged_actions
Audit table populated by database/application audit paths.
Key fields:
event_idschema_nametable_nameuser_idaction_timestampactionrow_datachanged_fieldsqueryclient_ipapplication_name
Notes:
- V28 widened the
actioncolumn. - V30+ migrations add auth/RLS-related grants and helper functions.
5.17 chat_conversations
AI assistant conversation storage.
Key fields:
iduser_idorganization_idmessagesupdated_atversiondeleted_at
5.18 beta_interests
Public/beta interest capture.
Key fields:
idemailcompany_sizeuse_casesourcecreated_atversion
5.19 leads
Lead capture records from public/landing flows.
Key fields:
idnameemailcompanyphonecountrymessagelead_sourceipuser_agentstatuscreated_at
5.20 stripe_webhook_events
Payment provider webhook idempotency/audit log.
Key fields:
idevent_typeorganization_idpayloadprocessed_aterror
5.21 adapter_config
Per-market integration adapter toggles.
Key fields:
idmarketadapter_typeadapter_nameenabledreasonupdated_atupdated_by
Notes:
- Added by V25.
- Used to control market adapters such as e-invoice integrations.
5.22 schema_version
Legacy/internal schema marker table mapped by Exposed.
Key fields:
versionapplied_atdescription
Notes:
- Flyway remains the migration authority. This table is not a replacement for Flyway history.
6. Cross-Cutting Conventions
UUID identifiers
Most business tables use UUID primary keys. Public API paths expose UUID strings for resource identifiers.
Soft deletion
Several tenant/business tables include deleted_at. Application queries should exclude soft-deleted rows unless a route is explicitly designed for archive/audit use.
Optimistic version field
Many tables include a version field. Preserve it when adding update paths and migrations.
Money
Money columns are stored as decimal/numeric values with explicit currency fields. base_amount fields support organization base-currency reporting.
Country and market support
Market-specific support currently includes HR/RS/BA concepts across country constraints, tax rates, chart-of-accounts migrations, SEF fields, and adapter configuration.
7. Operational Procedures
Add a table or column
Org-firstCreatecompositeaindexes:newEveryFlywayquerymigrationfilters byorganizationIdfirst — it must bewith theleftmostnextcolumnversion.- Add or update the corresponding Exposed mapping in
all multi-column indexes.Tables.kt. StatusUpdate+services/routes/testsdatethatcombos:useListtheendpointsnewcombinefield.- Update
filter + date sort, so(orgId, status, date)triples cover both filterOpenAPI andORDERbackendBY.docs if API shape changes. ForeignRunkeyFlywayindexes: Prisma creates FK indexes automatically; verify withvalidation/migration in\d tableNamepsql.the intended environment.CoveringCaptureindexes:evidenceForforreportMC/PRqueries that fetch only a few columns, consider partial indexes in Phase 2 (e.g.,WHERE status = 'paid').review.
QueryChange Performancean Targets
existing Audit Log Partitioning Strategy
Why Partition LoggedAction?migration
The LoggedAction table is append-only and retains data for 7 years (financial compliance requirement). At 10 requests/minute per organization, a 100-org instance generates ~500K audit rows/month. After 3 years: ~18M rows.
Without partitioning: sequential scans on LoggedAction become slow. With partitioning: queries can prune partitions by year.
Partitioning Approach: Range by Year
-- Convert LoggedAction to range-partitioned table (PostgreSQL 11+)
-- Execute ONCE during Phase 2 setup, before data volume grows
CREATE TABLE "LoggedAction_partitioned"
PARTITION OF "LoggedAction" (
-- same columns
) PARTITION BY RANGE (EXTRACT(YEAR FROM "createdAt"));
-- Annual partitions
CREATE TABLE "LoggedAction_2026" PARTITION OF "LoggedAction_partitioned"
FOR VALUES FROM (2026) TO (2027);
CREATE TABLE "LoggedAction_2027" PARTITION OF "LoggedAction_partitioned"
FOR VALUES FROM (2027) TO (2028);
-- etc.
Prisma Compatibility
Prisma doesDo not nativelyedit manageit. PostgreSQL table partitioning. Strategy:Instead:
DefineCreatebaseatablenew forward migration.- Explain the compatibility path in
theschema.prisma(noPR/MCpartitioning directive)evidence. ApplyValidatepartitioning viaprisma db executewith raw SQL after initial migrationMaintain partition creation ason ayearlynon-productionoperationstargettaskbefore(or usepg_partmanextension)promotion.
ArchiveRepair StrategyFlyway (7-Yearmetadata Retention)drift
YearOnly 1-7:perform Activerepair partitionsafter inall PostgreSQLof (Railwaythese EUare Frankfurt)captured:
Year
7+:- Target
Archiveidentity: partitionproject, toinstance, colddatabase, storageenvironment.
(Cloudflare- Flyway
R2validate Glacieroutput tier)showing Deleteexact fromdrift.
PostgreSQL- Schema
Retainchecks R2proving archivesthe live schema matches expected intent.
- Written runbook and abort conditions.
- Repair transcript.
- Post-repair validate/migrate/info output.
- Deployment or smoke evidence if the drift blocked CI/CD.
MC #101509 is the reference example for GDPRthis compliance period
Trigger: Automated yearly job checks MIN(createdAt) in oldest partition. If > 7 years: export to R2, drop partition.
MVP Approach (Phase 1)
For MVP: no partitioning required. Single table with composite index (organizationId, tableName, createdAt) sufficient for < 1M rows. Implement partitioning in Phase 2 before reaching 5M rows.flow.
8. Validation Checklist
EndBefore ofmarking Databasedatabase Schemadocumentation current:
Tables.kttable inventory reviewed.- Flyway migration directory reviewed.
- No stale deployment assumptions remain in this document.
- No legacy ORM workflow is presented as active.
- OpenAPI/API docs updated when endpoint shapes changed.
- Environment-specific migration claims cite evidence.
9. Known Follow-Ups
- Keep
docs/backend/openapi.yamlaligned with implemented Ktor routes. - Keep
docs/backend/API-REFERENCE.mdaligned with OpenAPI. - Keep deployment docs aligned with GCP Cloud Run and Cloud SQL reality.
- Consider generating a schema snapshot from a migrated Cloud SQL-compatible database for future reviews.