Database Schema
BilkoDrop Database Schema
Status:IMPLEMENTEDLast verified:2026-05-20Canonical backend:Source:apps/apisrc/shared/db/schema.tsKotlin/Ktor(DrizzleserviceORMDatabase:schemaPostgreSQL—on GCP Cloud SQL for deployed environmentsSchemasingle source oftruth:truthFlywayforSQLallmigrations + Exposed table mappingsenvironments)
Overview
ThisDrop documentuses describesPostgreSQL 16 as the database schema currently used by the Bilko Kotlin/Ktor API.
It replaces the older ORM-erasole database notesengine in all environments (development, CI, staging, production).
Database access is via Drizzle ORM. There is no SQLite dependency and mustno bedual-driver kept aligned with:abstraction.
FlywayLocalmigrations:dev: PostgreSQL 16 in Docker (), port 5433apps/api/src/main/resources/db/migration/docker compose up -dExposedCI:tablePostgreSQLmappings:16service container in GitHub Actionsapps/api/src/main/kotlin/no/alai/bilko/models/Tables.ktRoute/serviceProduction:behaviour:PostgreSQL 16 on AWS RDS (apps/api/src/main/kotlin/no/alai/bilko/routes/db.t3.smalland)apps/api/src/main/kotlin/no/alai/bilko/services/EnvironmentSchemamapping:definition:(Drizzle schema, TypeScript, PostgreSQL-native)infrastructure/gcp/ENV-MATRIX.mdsrc/shared/db/schema.ts- Migrations: managed by
drizzle-kit
DoSee notADR-014 treatfor generatedthe diagrams,full frontendrationale.
Total definitions,tables: or19 archived(12 deploymentcore notes+ as7 database authority.compliance)
1. Architecture OverviewTables
Bilko is a multi-tenant accounting SaaS. The active backend stores tenant data in PostgreSQL and scopes business tables by organization_id where appropriate.
users
Runtime stack:
API:Kotlin/KtorSQL migration engine:FlywayKotlin SQL mapping:JetBrains ExposedDatabase engine:PostgreSQLDeployed DB platform:GCP Cloud SQL- Primary
migrationusercommand path:Cloud Build / backend Gradle Flyway tasks
The schema is forward-only. Applied migrations 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 postflight validation.
Relationship overview
erDiagram
organizations ||--o{ users : owns
organizations ||--o{ accounts : owns
organizations ||--o{ contacts : owns
organizations ||--o{ invoices : owns
organizations ||--o{ expenses : owns
organizations ||--o{ transactions : owns
organizations ||--o{ bank_accounts : owns
organizations ||--o{ recurring_invoices : owns
organizations ||--o{ adapter_config : configures
organizations ||--o{ stripe_webhook_events : receives
users ||--o{ refresh_tokens : has
users ||--o{ invoices : creates
users ||--o{ expenses : creates
users ||--o{ transactions : creates
users ||--o{ logged_actions : actor
account_types ||--o{ accounts : classifies
accounts ||--o{ accounts : parent
accounts ||--o{ invoice_items : revenue_account
accounts ||--o{ expenses : expense_account
accounts ||--o{ bank_accounts : ledger_account
accounts ||--o{ transactions : debit_account
accounts ||--o{ transactions : credit_account
contacts ||--o{ invoices : customer
contacts ||--o{ expenses : vendor
contacts ||--o{ recurring_invoices : template_customer
invoices ||--o{ invoice_items : contains
bank_accounts ||--o{ bank_transactions : imports
transactions ||--o{ bank_transactions : reconciles
currencies ||--o{ exchange_rates : base_currency
currencies ||--o{ exchange_rates : target_currency
2. Source-of-Truth Rules
Add schema changes via new Flyway migrations only.Use the next available version underapps/api/src/main/resources/db/migration/.Never rewrite an already-applied migration in demo, staging, or production.
Update Exposed mappings in the same change.Tables.ktshould match the migrated database surface used by routes/services.
Update API and docs together.If a schema change alters request/response shapes, updatedocs/backend/openapi.yamland relevant backend docs.
Validate with Flyway before deploy promotion.A valid deploy target must pass Flyway validation before the API is considered healthy.
Keep environment identity explicit.Staging, demo, and production databases are separate targets. Migration or repair work must state which database is being touched.
3. Migration Inventory
As of 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.
Important migration groups:accounts.
| Constraints | Default | ||
|---|---|---|---|
| PRIMARY |
- | ||
| UNIQUE |
- | ||
| NOT |
- | ||
| NOT |
- | ||
| NOT |
- | ||
| phone | TEXT | - | NULL |
| date_of_birth | TEXT | - | NULL |
| kyc_status | TEXT | CHECK('pending','approved','rejected') | 'pending' |
| role | TEXT | CHECK('user','merchant') | 'user' |
| created_at | TEXT | - | CURRENT_TIMESTAMP |
OperationalID noteformat: from MC #101509:
Staging checksum drift was detected for V22, V25, V26, and V28.Schema checks proved checksum-only drift before repair.Controlled Flyway repairusr_+migrate16broughthexstagingcharsto version 35 andflyway validatepassed.The authoritative stage Cloud Build trigger then succeeded.
4. Tenant and Security Model
Most business data is scoped(generated by organization_idrandomId("usr") 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 workinthe V17 and V30+ series.
utils-server.ts:4)
ApplicationPassword codehashing: mustbcrypt setwith the12 correctrounds organization/user context before querying tenant-scoped tables. Any new table containing tenant data should include (organization_idutils-server.ts:8-11 unless it is intentionally global reference data.)
5. Tables
The following table inventory is derived from Tables.kt and active migrations.
Column notationrecipients
TheSaved detailedremittance type/constraintrecipients authorityper remains Flyway SQL plus Tables.kt. This document uses these shorthand types for quick review:user.
| Constraints | Default | ||
|---|---|---|---|
id |
PRIMARY |
- | |
user_id |
NOT |
- | |
name |
NOT |
- | |
country |
NOT |
- | |
currency |
NOT NULL | - | |
bank_account |
NOT |
- | |
bank_name |
- | NULL | |
created_at |
- | CURRENT_TIMESTAMP |
CommonSupported lifecyclecountries: columnsRS, areBA, PL, PK, TR (enforced at API level, not DB level)
Index: created_atidx_recipients_user, on updated_atuser_id, version, and deleted_at where present.
5.1 organizationsmerchants
TenantRegistered rootmerchant table.
Keyfor fields:
idnameregistration_numbervat_number,vat_country,vat_registered,vat_ratefirm_typebase_currencycountry,languagefiscal_year_startlogo_urlsecurity_settingssubscription/trial fields:plan_tier,quota_invoices_month,quota_contacts,quota_users,stripe_customer_id,stripe_subscription_id,trial_started_at,trial_ends_atlifecycle fields:created_at,updated_at,version,deleted_at
Column summary:payments.
| Column |
Constraints | Default | |
|---|---|---|---|
|
PRIMARY |
- | |
| NOT |
- | ||
|
NOT |
- | |
|
UNIQUE |
- | |
| - | NULL | ||
NOT |
- | ||
| fee_rate | REAL | - | 0.01 |
| status | TEXT | - | 'active' |
| created_at | TEXT | - | CURRENT_TIMESTAMP |
Notes:Index: idx_merchants_org on org_number
countrytransactions
isAll
constrainedfinancialbytransactionsmigration history(remittances andusedQRby 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_atinvite fields:invite_token,invite_expires_atstatusis_platform_adminlifecycle fields:created_at,updated_at,version,deleted_at
Column summary:payments).
| Column |
Constraints | Default | |
|---|---|---|---|
|
PRIMARY |
- | |
|
NOT → users(id) |
- | |
|
NOT NULL, CHECK('remittance','qr_payment') | - | |
| CHECK('processing','completed','failed') | 'processing' | ||
| NOT |
- | ||
| - | 'NOK' | ||
| fee | REAL | - | 0 |
| recipient_id | TEXT | FK → recipients(id) | NULL |
| merchant_id | TEXT | FK → merchants(id) | NULL |
| send_amount | REAL | - | NULL |
| send_currency | TEXT | - | NULL |
| receive_amount | REAL | - | NULL |
| receive_currency | TEXT | - | NULL |
| exchange_rate | REAL | - | NULL |
| created_at | TEXT | - | CURRENT_TIMESTAMP |
| completed_at | TEXT | - | NULL |
Indexes: idx_transactions_user on user_id, idx_transactions_merchant on merchant_id
Notes:
PasswordRemittancesandhave2FArecipient_idbehaviourset,ismerchant_idimplementedNULL- QR
thepaymentsKotlinhaveauthmerchant_idservices.set,recipient_idNULL is_platform_adminsend_*was/addedreceive_*by/V26.exchange_ratefields are populated for remittances only
5.3 refresh_tokensexchange_rates
SessionCurrency refresh-tokenexchange storage.
Key(from 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_activelifecycle fields:created_at,updated_at,version,deleted_at
Notes:
Market-specific chart additions exist for BA and RS.Account hierarchy is represented withNOK).parent_account_id
5.6 contacts
Customers and vendors.
Key fields:
idorganization_idtypenameemail,phoneregistration and tax identifiers:registration_number,vat_number,jmbg,jmbg_hash,oib,oib_hashaddress fields:address_line1,address_line2,city,postal_code,countrycurrency_codepayment_termsnotesis_activelifecycle 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_numberdates:invoice_date,due_date,sent_at,viewed_at,paid_atmoney fields:currency_code,exchange_rate,subtotal,tax_amount,discount_amount,total_amount,base_amountstatusnotes,terms,pdf_urle-invoice fields:is_reverse_charge,sef_id,sef_document_id,sef_status,sef_submitted_at,sef_accepted_atcreated_bylifecycle fields:created_at,updated_at,version,deleted_at
Column summary:
| Column |
Constraints | Default | |
|---|---|---|---|
|
PRIMARY |
AUTOINCREMENT | |
| - | 'NOK' | ||
| NOT |
- | ||
| NOT |
- | ||
| CURRENT_TIMESTAMP |
Notes:
- data
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_datemoney fields:currency_code,exchange_rate,amount,base_amount,tax_amountcategorypayment_methodaccount_iddescriptionreceipt_urlstatusapproval/payment fields:approved_by,approved_at,paid_atcreated_bylifecycle fields:created_at,updated_at,version,deleted_at
Column summary:(db.ts:531-545):
11.7 |
|
0.089 |
Notes:
Expense status enum support was added by V19.
5.11 transactionsbank_accounts
GeneralLinked ledgerbank transactions.accounts (AISP — Open Banking read in production, mock balances in dev).
KeyPass-throughfields:model: Drop NEVER holds customer money. Thebalancecolumn stores the last AISP-read balance from the user's real bank account — it is a cached read-only value, not a Drop-held balance. In dev/demo mode, mock balances are seeded for testing.
idorganization_idtransaction_datedescriptiondebit_account_idcredit_account_idmoney fields:amount,currency_code,exchange_rate,base_amountsource reference:reference_type,reference_idlock/reconciliation fields:locked,locked_at,reconciled,reconciled_atnotescreated_bycreated_atversiondeleted_at
Column summary:
| Column |
Constraints | Default | |
|---|---|---|---|
|
PRIMARY |
- | |
| NOT |
- | ||
| NOT |
- | ||
| NOT |
- | ||
| - | NULL | ||
| - | 0 | ||
| currency | TEXT | - | 'NOK' |
| is_primary | INTEGER | - | 0 |
| connected_at | TEXT | - | CURRENT_TIMESTAMP |
5.12
Index: on bank_accountsidx_bank_accounts_useruser_id
cards (FUTURE — feature-flagged)
BankNote:accountsCardslinkedare a FUTURE feature, gated behind feature flags (all default toledgerfalse).accounts.
Key fields:
idorganization_idaccount_idbank_nameaccount_numberibancurrency_codecurrent_balanceis_activelifecycle fields:created_at,updated_at,version,deleted_at
5.13bank_transactions
Imported or entered bank movements.
Key fields:
idbank_account_idtransaction_dateamountdescriptionreferencereconciledmatched_transaction_idcreated_atversiondeleted_at
5.14currencies
Currency reference data.
Key fields:
codenamesymboldecimal_placesis_activecreated_atversion
5.15exchange_rates
Foreign-exchange rates.
Key fields:
idbase_currencytarget_currencyrateeffective_datesourcelast_updatedversiondeleted_at
5.16logged_actions
AuditThis tablepopulatedexistsbyindatabase/applicationtheauditschemapaths.but is not actively used until a card issuing partner is integrated.
ColumnVirtual summary:and physical payment cards.
| Column |
Constraints | Default | |
|---|---|---|---|
TEXT |
PRIMARY |
- | |
| NOT NULL, FK → users(id) | - | ||
| CHECK('virtual','physical') | 'virtual' | ||
| NOT |
- | ||
| token_ref | TEXT | - | NULL |
| expiry | TEXT | NOT NULL | - |
| status | TEXT | CHECK('active','frozen','cancelled') | 'active' |
| shipping_address | TEXT | - | NULL |
| created_at | TEXT | - | CURRENT_TIMESTAMP |
| pin_hash | TEXT | - | NULL (added via runtime migration in cards/[id]/pin/route.ts:51-53) |
KeyIndex: fields:
idx_cards_user - on
event_idschema_nametable_nameuser_idaction_timestampactionrow_datachanged_fieldsqueryclient_ipapplication_name
Notes:
V28 widened theactioncolumn.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 sef_webhook_events
SEF status webhook idempotency/audit log added by V36. Because SEF does not expose a documented immutable event ID, the API computes a SHA-256 idempotency key from the parsed SEF payload and raw body before calling SefService.handleWebhook().
Key fields:
id— SHA-256 idempotency key; primary keysef_invoice_idstatusstatus_datepayloadprocessing_status—processing,processed, orfailedprocessed_aterrorcreated_at
Notes:
Duplicate webhook deliveries with the same idempotency key return200withduplicate=trueand are not reprocessed once processing is in progress or complete.Failed events are markedfailed; a later duplicate delivery can retry processing.Signature verification still happens first viaX-Sef-Signature/SEF_WEBHOOK_SECRET.
5.22 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.23 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 columnsessions
Createsessiona new Flyway migration with the next version.Add or update the corresponding Exposed mapping inTables.kt.Update services/routes/tests that use the new field.Update OpenAPI and backend docs if API shape changes.Run Flyway validation/migration in the intended environment.Capture evidencetracking forMC/PRrevocationreview.
JWT
Change an existing applied migration
Do not edit it. Instead:
Create a new forward migration.Explain the compatibility path in the PR/MC evidence.Validate on a non-production target before promotion.
Repair Flyway metadata drift
Only perform repair after all of these are captured:
Target identity: project, instance, database, environment.Flyway validate output showing exact drift.Schema checks proving the 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 this flow.
8. Validation Checklist
Before marking database documentation 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. Index and Performance Strategy
The exact index inventory is migration-defined and should be inspected with psql against the target database when diagnosing query plans. The application design depends on these index principles:support.
| Constraints | Default | ||
|---|---|---|---|
| PRIMARY |
- | ||
| NOT |
- | ||
| NOT |
- | ||
| - | CURRENT_TIMESTAMP | ||
| NOT |
- | ||
| |||
| 0 |
PerformanceIndexes: targetsidx_sessions_user on user_id, idx_sessions_token on token_hash
Token hash: SHA-256 of the JWT string (auth.ts:59)
notifications
In-app notifications.
| Column | Type | Constraints | Default |
|---|---|---|---|
| id | TEXT | PRIMARY KEY | - |
| user_id | TEXT | NOT NULL, FK → users(id) | - |
| type | TEXT | NOT NULL | - |
| title | TEXT | NOT NULL | - |
| body | TEXT | NOT NULL | - |
| read | INTEGER | - | 0 |
| created_at | TEXT | - | CURRENT_TIMESTAMP |
Index: idx_notifications_user on user_id
settings
Per-user preferences.
| Column | Type | Constraints | Default |
|---|---|---|---|
| user_id | TEXT | PRIMARY KEY, FK → users(id) | - |
| currency | TEXT | - | 'NOK' |
| language | TEXT | - | 'nb' |
| push_enabled | INTEGER | - | 1 |
| email_enabled | INTEGER | - | 1 |
| updated_at | TEXT | - | CURRENT_TIMESTAMP |
spending_limits (FUTURE — feature-flagged)
Note: Tied to the cards feature. Only active when card feature flags are enabled.
Card spending limits.
| Column | Type | Constraints | Default |
|---|---|---|---|
| id | TEXT | PRIMARY KEY | - |
| user_id | TEXT | NOT NULL, FK → users(id) | - |
| card_id | TEXT | FK → cards(id) | NULL |
| limit_type | TEXT | NOT NULL | - |
| amount | REAL | NOT NULL | - |
| currency | TEXT | - | 'NOK' |
| created_at | TEXT | - | CURRENT_TIMESTAMP |
Indexes: idx_spending_limits_user on user_id, idx_spending_limits_card on card_id
Limit types (API-enforced): daily, weekly, monthly, transaction
rate_limits
Persistent rate limiting store.
| Column | Type | Constraints | Default |
|---|---|---|---|
| key | TEXT | PRIMARY KEY | - |
| count | INTEGER | NOT NULL | - |
| reset_at | INTEGER | NOT NULL | - |
Used by middleware.ts:rateLimit() for product-facingIP-based paths:rate limiting. Expired entries are cleaned on each call (middleware.ts:11).
Compliance & GDPR Tables
Added: 2026-02-16 (compliance infrastructure)
These tables support Drop's compliance requirements for Norwegian financial services regulation, GDPR, and AML/KYC requirements per hvitvaskingsloven.
audit_log
User action audit trail for compliance and security monitoring.
| Column | Type | Constraints | Default |
|---|---|---|---|
| id | TEXT | PRIMARY KEY | - |
| timestamp | TEXT | - | CURRENT_TIMESTAMP |
| user_id | TEXT | FK → users(id) | NULL |
| action | TEXT | NOT NULL | - |
| resource_type | TEXT | - | NULL |
| resource_id | TEXT | - | NULL |
| details | TEXT | - | NULL |
| ip_address | TEXT | - | NULL |
| user_agent | TEXT | - | NULL |
Indexes: idx_audit_log_user on user_id, idx_audit_log_timestamp on timestamp, idx_audit_log_action on action
Purpose: Tracks all significant user actions (login, transaction, settings change, etc.) for audit purposes.
aml_alerts
AML (Anti-Money Laundering) transaction monitoring alerts.
| Column | Type | Constraints | Default |
|---|---|---|---|
| id | TEXT | PRIMARY KEY | - |
| user_id | TEXT | NOT NULL, FK → users(id) | - |
| alert_type | TEXT | NOT NULL | - |
| severity | TEXT | NOT NULL, CHECK('low','medium','high','critical') | - |
| transaction_id | TEXT | FK → transactions(id) | NULL |
| details | TEXT | - | NULL |
| status | TEXT | CHECK('open','investigating','resolved','escalated','filed') | 'open' |
| reviewed_by | TEXT | - | NULL |
| reviewed_at | TEXT | - | NULL |
| created_at | TEXT | - | CURRENT_TIMESTAMP |
Indexes: idx_aml_alerts_user on user_id, idx_aml_alerts_status on status
Purpose: Records suspicious transaction patterns flagged by AML monitoring rules (e.g., structuring, velocity, high-risk corridors).
str_reports
STR (Suspicious Transaction Reports) filed with financial authorities.
| Column | Type | Constraints | Default |
|---|---|---|---|
| id | TEXT | PRIMARY KEY | - |
| user_id | TEXT | NOT NULL, FK → users(id) | - |
| alert_id | TEXT | FK → aml_alerts(id) | NULL |
| report_type | TEXT | NOT NULL | - |
| status | TEXT | CHECK('draft','submitted','acknowledged') | 'draft' |
| filed_at | TEXT | - | NULL |
| reference_number | TEXT | - | NULL |
| details | TEXT | - | NULL |
| created_at | TEXT | - | CURRENT_TIMESTAMP |
Purpose: Tracks STRs filed with Økokrim/EFE (Norwegian financial intelligence unit) per hvitvaskingsloven requirements.
screening_results
Results from sanctions/PEP (Politically Exposed Persons) screening.
| Column | Type | Constraints | Default |
|---|---|---|---|
| id | TEXT | PRIMARY KEY | - |
| user_id | TEXT | NOT NULL, FK → users(id) | - |
| screening_type | TEXT | NOT NULL, CHECK('pep','sanctions','adverse_media') | - |
| provider | TEXT | - | NULL |
| result | TEXT | NOT NULL, CHECK('clear','match','potential_match','error') | - |
| match_details | TEXT | - | NULL |
| screened_at | TEXT | - | CURRENT_TIMESTAMP |
Indexes: idx_screening_user on user_id
Purpose: Stores results from automated screening against PEP lists, sanctions lists (OFAC, UN, EU), and adverse media databases.
consents
GDPR consent tracking for user data processing.
| Column | Type | Constraints | Default |
|---|---|---|---|
| id | TEXT | PRIMARY KEY | - |
| user_id | TEXT | NOT NULL, FK → users(id) | - |
| consent_type | TEXT | NOT NULL | - |
| granted | INTEGER | NOT NULL | 1 |
| granted_at | TEXT | - | CURRENT_TIMESTAMP |
| withdrawn_at | TEXT | - | NULL |
| ip_address | TEXT | - | NULL |
Indexes: idx_consents_user on user_id
Consent Types (API-enforced): terms, privacy, marketing, cookies_analytics, cookies_marketing
Purpose: Tracks when users grant or withdraw consent for different types of data processing, with IP address as proof of consent action.
data_access_requests
GDPR data access/erasure/rectification requests (Art. 15-17).
| Column | Type | Constraints | Default |
|---|---|---|---|
| id | TEXT | PRIMARY KEY | - |
| user_id | TEXT | NOT NULL, FK → users(id) | - |
| request_type | TEXT | NOT NULL, CHECK('export','erasure','rectification','restriction') | - |
| status | TEXT | CHECK('pending','processing','completed','rejected') | 'pending' |
| requested_at | TEXT | - | CURRENT_TIMESTAMP |
| completed_at | TEXT | - | NULL |
| download_url | TEXT | - | NULL |
| notes | TEXT | - | NULL |
Indexes: idx_data_requests_user on user_id
Purpose: Tracks GDPR data subject access requests. export requests generate full data export, erasure triggers account deletion.
complaints
Customer complaints per Finansavtaleloven §3-53 (15-day response requirement).
| Column | Type | Constraints | Default |
|---|---|---|---|
| id | TEXT | PRIMARY KEY | - |
| user_id | TEXT | NOT NULL, FK → users(id) | - |
| category | TEXT | NOT NULL | - |
| subject | TEXT | NOT NULL | - |
| description | TEXT | NOT NULL | - |
| status | TEXT | CHECK('received','investigating','resolved','escalated') | 'received' |
| resolution | TEXT | - | NULL |
| created_at | TEXT | - | CURRENT_TIMESTAMP |
| resolved_at | TEXT | - | NULL |
Indexes: idx_complaints_user on user_id, idx_complaints_status on status
Categories (API-enforced): transaction, service, fees, privacy, technical, other
Purpose: Formal complaint logging system to ensure compliance with Norwegian financial services law requiring 15 business day response time.
Database Access Layer
Source: db.ts
Data Access Layer
The database access layer is Drizzle ORM (src/shared/db/schema.ts). The old db.ts
dual-driver abstraction has been removed (see ADR-014).
Use Drizzle query builder or the sql template tag for raw queries:
import { db } from '@drop/shared/db';
import { users } from '@drop/shared/db/schema';
import { eq } from 'drizzle-orm';
// Type-safe query
const user = await db.select().from(users).where(eq(users.id, userId)).limit(1);
// Raw SQL escape hatch (PostgreSQL syntax, $1 params not needed — Drizzle handles binding)
import { sql } from 'drizzle-orm';
const result = await db.execute(sql`SELECT id FROM users WHERE email = ${email}`);
Migrations are managed by drizzle-kit:
cd src/shared && npx drizzle-kit generate # Generate migration file
cd src/shared && npx drizzle-kit push # Push schema to dev database
make db-push # Shortcut (from repo root)
Seed Data
When exchange_rates table is empty, seedData() (db.ts:530) populates:
Tenant-scoped6listexchangeendpointsrateshouldcorridorsavoid(NOKfull-table→scansRSD,acrossBAM,organizations.PLN, PKR, TRY, EUR)Month/quarterDemoreportdataqueries(whenshouldNODE_ENVbe!==bounded"production"byororganizationSEED_DEMO=true):and- 1
range.demo user (usr_demo1, [email protected], role: merchant) Background3reconciliation/exportrecipientsjobs(Serbia,mayBosnia,use broader scans, but should be batchable and observable.Turkey)Any1newmerchanthigh-cardinality(AhmetovfieldKebab)- 3
intransactionsfilters(2shouldremittances,include1anQRindexpayment) - 2
inbanktheaccountsmigration(DNBPR.primary with 45,230 NOK, SpareBank 1 with 12,800 NOK)
dateuseddecisionWhen adding an index:Add it in a new Flyway migration.Explain the route/report it supports.Verify withEXPLAINor a representative query when data volume makes the risk material.
10. Audit Log Scaling and Retentionlogged_actionscan grow faster than ordinary tenant tables. Current documentation stance:The active schema keeps audit rows in PostgreSQL and records actor, target table, action, row data, changed fields, query text, client IP, and application name.V28 widened the action field to support current action labels.No partitioning migration is currently documented as applied inTables.kt/Flyway source of truth.
Future scaling decision:If audit volume threatens report/API latency or storage budgets, introduce an explicit Flyway migration for partitioning or archival.The migration must include retention policy, query impact, backfill plan, and restore/audit requirements.Until that migration exists, do not describe partitioning as active behaviour.
11. Known Follow-UpsKeepdocs/backend/openapi.yamlaligned with implemented Ktor routes.Keepdocs/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.
- 1