Core / Shared
Read Conventions first - the rules there (tenant scoping, status vocabularies, numbering) apply throughout this page without being repeated per table.
businesses
The tenant root. Also carries letterhead identity facts (name/logo/address/TIN), not operational preferences.
id- uuid - PKname- text NOT NULLcreated_atlogo_url- text, nullableaddress- text NOT NULL DEFAULT ‘’ - required on Sri Lanka IRD VAT tax invoices (Gazette 2481/22)phone,email,tax_id- text NOT NULL DEFAULT ‘’ (tax_id= supplier TIN)admin_unlock_password_hash- text NULL - shared step-up password gating Full grants on Administration permissions
branches
Physical/organisational locations under a business.
id,business_id→ businessesnameNOT NULL;colortext DEFAULT ‘#6b7280’ (UI tag colour)code,landmark,city,state,zip_code,country,mobile,alternate_contact_number,email,website- text NOT NULL DEFAULT ‘’default_warehouse_id→ warehousesdefault_price_list_id→ price_lists - POS at this branch prices from this list instead ofitems.default_selling_priceis_headquarters- boolean DEFAULT false - partial UNIQUE index(business_id) WHERE is_headquarters: at most one HQ branch per businessallow_oversell- boolean NULL - override; NULL = inherit business setting (precedence item → branch → business)created_at
users
id,business_id→ businessesemail- text NOT NULL UNIQUE globally (not per business)password_hashfirst_name,last_name- text NOT NULL DEFAULT ‘’ (replaced the original singlenamecolumn)mobile- for SMS OTP password resetemployee_id,avatar_urlis_super_adminboolean DEFAULT false;is_activeboolean DEFAULT true (login blocks on false)locked_reason- text NULL - distinguishes automatic lockout from manual deactivationis_ai_native- boolean DEFAULT false - unlocks the AI Business Advisor capabilitygrade_id→ employee_grades;position_id→ positionsapproval_limit_override- numeric(14,2) NULL - effective limit = COALESCE(this, grade default, 0)totp_secret/totp_enabled/totp_enrolled_at- web-login 2FApos_totp_secret/pos_totp_enabled/pos_totp_enrolled_at- deliberately separate secret from web 2FA; cached decryptable on the offline terminalpos_login_pin_hash- the PIN a cashier signs in withpos_pin_hash- authorization-only PIN (approving someone else’s void/discount); never interchangeable with the login PINpos_card_token_hash- badge-scan authorization; random opaque token, not a PINshow_on_pos_terminal- boolean DEFAULT truecurrent_session_id- uuid NULL - single-session-per-user; embedded as a JWT claim, stale value = signed outcreated_at
permissions
Global catalogue of permission keys, not business-scoped. ~270 keys, e.g. purchase_order.add, purchase_order.post.
key- text - PK;descriptionNOT NULL
roles
id,business_id,name,created_at
role_permissions
SAP B1-style 3-tier General Authorization.
role_id→ roles CASCADE,permission_key→ permissions(key) - composite PKlevel- text DEFAULT ‘full’ - CHECK IN (‘read’,‘full’)
user_branches
Which branches a user can work in, and with which role there.
user_id→ users CASCADE,branch_id→ branches CASCADE - composite PKrole_id→ roles NOT NULL
audit_logs
One row per changed field. Tamper-evident hash chain.
id,business_id,user_id→ users (nullable - system writes)entity_typetext NOT NULL (e.g. ‘item’),entity_iduuid NOT NULL,entity_labelfieldtext NOT NULL;old_value/new_valuetext NULLseq- bigserial - strict deterministic ordering the hash chain links against (created_at can tie)prev_hash,record_hash- text NULL - NULL on rows written before the hash chain was added; chain starts clean from therecreated_at- Indexes:
(business_id, entity_type, entity_id, created_at DESC),(business_id, seq DESC)
notifications
In-app notifications, one row per (user, event). Stores no route - the frontend maps entity_type → route.
id,business_id,user_id→ users NOT NULLtype,titleNOT NULL;bodyentity_type- text DEFAULT ‘’ (reuses document-type strings: ‘purchase_order’, ‘ar_invoice’, …);entity_iduuid NULLis_readboolean DEFAULT falsecreated_at; partial index(user_id) WHERE is_read = falsefor the badge count
business_settings
One row per business. Absorbed the old standalone Batch Settings page.
business_id- uuid PKbatch_number_formatDEFAULT'{DATE:YYYYMMDD}-{GRN}-{SEQ:4}'allow_oversellboolean DEFAULT true - business-wide default (branch/item may override)start_date;timezoneDEFAULT ‘UTC’financial_year_start_monthsmallint DEFAULT 1 CHECK BETWEEN 1 AND 12stock_accounting_methodDEFAULT ‘fifo’ - CHECK IN (‘fifo’) onlytransaction_edit_daysint DEFAULT 60date_formatDEFAULT ‘mm/dd/yyyy’;time_formatCHECK IN (‘12h’,‘24h’)currency_symbol_placementCHECK IN (‘before’,‘after’);currency_precisionsmallint 2;quantity_precisionsmallint 3default_profit_percentnumeric(5,2)default_sale_discountnumeric(5,2);default_sale_tax_group_id→ tax_groupssales_item_addition_methodCHECK IN (‘increase_quantity’,‘new_line’)amount_rounding_methodCHECK IN (‘none’,‘round_up’,‘round_down’,‘round_nearest’)sales_price_is_minimumbooleanbarcode_prefix,barcode_sku_lengthsmallint 6,barcode_qty_integer_lengthsmallint 2,barcode_qty_fractional_lengthsmallint 3 - weighing-scale barcode layoutgift_voucher_expiry_monthsinteger >= 0 - 0 = no default policyloyalty_earn_rate_lkr_per_pointnumeric(10,2) - 0 = offloyalty_redeem_value_per_pointnumeric(10,4) - 0 = offchange_money_enabledboolean;change_money_auto_redeem_thresholdnumeric(10,2) DEFAULT 10pos_print_cancel_receipt/pos_print_void_receiptboolean DEFAULT truepos_auto_print_sale_receiptboolean DEFAULT false - kiosk-style auto printupdated_at
accounting_settings
Per-business fixed G/L account targets. One row per business.
business_id- uuid PK- All
uuid → gl_accountsnullable:goods_receipt_offset_account_id,goods_issue_offset_account_id,opening_balance_offset_account_id,inventory_count_offset_account_id,grn_offset_account_id,goods_return_offset_account_id,accounts_payable_account_id,down_payment_offset_account_id,accounts_receivable_account_id,ar_deferred_revenue_account_id,loyalty_points_liability_account_id,loyalty_points_expense_account_id,charges_income_account_id,realized_exchange_gain_loss_account_id,unrealized_exchange_gain_loss_account_id,fixed_asset_disposal_gain_loss_account_id updated_at
gl_account_determination_defaults
Company-wide fallback accounts. Used only where the item’s chosen source leaves an account unconfigured.
business_id- uuid PK- 24 nullable
uuid → gl_accountscolumns:inventory_account_id,expense_account_id,revenue_account_id,cogs_account_id,allocation_account_id,variance_account_id,price_difference_account_id,negative_inventory_adjustment_account_id,inventory_offset_decrease_account_id,inventory_offset_increase_account_id,sales_returns_account_id,exchange_rate_difference_account_id,goods_clearing_account_id,gl_decrease_account_id,gl_increase_account_id,wip_inventory_account_id,wip_inventory_variance_account_id,wip_offset_account_id,inventory_offset_pl_account_id,expense_clearing_account_id,stock_in_transit_account_id,shipped_goods_account_id,sales_credit_account_id,purchase_credit_account_id updated_at
document_sequences
Legacy per-type counter, superseded by document_numbering_series but still used for batch_number and pos_terminal.
business_id,document_type- composite PK;next_numberinteger DEFAULT 1
document_numbering_series
Configurable prefix/suffix/period numbering per document type, optionally per branch.
id,business_id,document_typeNOT NULLbranch_id→ branches NULL - NULL = the default series for that typeprefix,suffixDEFAULT ‘’first_no/next_nointeger DEFAULT 1;last_noNULL (cap)period_indicatorDEFAULT ‘none’ - CHECK IN (‘none’,‘yearly’,‘monthly’)serial_digitsinteger DEFAULT 6 CHECK BETWEEN 0 AND 10is_lockedboolean DEFAULT falsecreated_at,updated_at- UNIQUE (business_id, document_type, branch_id) plus a partial unique index
(business_id, document_type) WHERE branch_id IS NULL- Postgres treats each NULL as distinct, so this partial index is what actually enforces “at most one default series per document type”
document_numbering_series_years
Per-year counter for a period_indicator = 'yearly' series.
id,series_id→ document_numbering_series CASCADE,year,next_no- UNIQUE (series_id, year)
document_numbering_series_periods
Same, keyed ‘YYYY-MM’, for a 'monthly' series.
id,series_idCASCADE,period,next_no- UNIQUE (series_id, period)
employee_grades
A named default monetary approval limit a user inherits.
id,business_id,name- UNIQUE (business_id, name)default_approval_limitnumeric(14,2) DEFAULT 0;created_at
positions
Job title, distinct from Grade; every position belongs to one grade, which supplies the approval limit.
id,business_id,name- UNIQUE (business_id, name)grade_id→ employee_grades NOT NULL;created_at
dashboard_widget_visibility
Presence of a row = that widget is visible to that position. Zero rows for a widget = visible to everyone.
id,business_id,widget_key,position_id→ positions - UNIQUE (business_id, widget_key, position_id)
print_templates
Per-business customized print HTML. A missing row means “use the built-in default”; deleting the row is how “Reset to default” works.
id,business_iddocument_type- CHECK IN 19 values: purchase_request, purchase_order, grn, ap_invoice, ap_down_payment_invoice, goods_return_request, goods_return, ap_debit_memo, grn_sticker, sales_quotation, sales_order, ar_invoice, delivery, sales_return_request, sales_return, ar_credit_memo, incoming_payment, outgoing_payment, outgoing_payment_cheque_handoverhtmlNOT NULL;updated_byNOT NULL,updated_at- UNIQUE (business_id, document_type)
print_paper_settings
Paper size per document type, deliberately separate from print_templates. Missing row = A4.
business_id,document_type- composite PK (same 19-value CHECK)paper_sizeDEFAULT ‘A4’ - CHECK IN (‘A4’,‘A5’,‘letter’,‘legal’)updated_byNOT NULL,updated_at
document_print_log
One row per actual print action (not per preview load) - lets the print view stamp “REPRINT” on the 2nd+ printout.
id,business_id,document_type,document_id,printed_by,printed_at- Index
(business_id, document_type, document_id)
sticker_sheet_settings
Named label-sheet geometry, in inches, calibrated by the operator.
id,business_id,name- UNIQUE (business_id, name);descriptionis_continuous_feedboolean DEFAULT falsemargin_top_in,margin_left_innumeric >= 0sticker_width_in,sticker_height_innumeric > 0paper_width_in,paper_height_innumeric NULL, > 0 when set - CHECK required unlessis_continuous_feedstickers_per_rowint > 0;row_distance_in,column_distance_innumeric >= 0stickers_per_sheetint > 0 - user-entered, deliberately not derived from geometryis_default- partial UNIQUE index(business_id) WHERE is_defaultis_active,created_at
Notification channel settings
- sms_settings -
business_idPK;mask,username,password,test_numberDEFAULT ‘’;register_close_notification_numberstext DEFAULT ‘’ (comma-separated; empty = feature off);updated_at - email_settings -
business_idPK;host,portint DEFAULT 587,username,password;encryptionDEFAULT ‘tls’ CHECK IN (‘none’,‘tls’,‘ssl’);from_address,from_name,test_email;updated_at - telegram_settings -
business_idPK;bot_token,chat_idDEFAULT ‘’;updated_at - telegram_alert_topics -
id,business_id,event_key,enabledboolean DEFAULT false,thread_idinteger NULL - UNIQUE (business_id, event_key) - whatsapp_settings -
business_idPK;phone_number_idtext - partial UNIQUE indexWHERE phone_number_id != ''(the inbound webhook carries only this, never a business_id);waba_id,access_token,app_secret,verify_tokenDEFAULT ‘’;is_enabledDEFAULT true;updated_at - whatsapp_authorized_numbers - a number only gets answers once linked to a real user account; questions run under that user’s own permissions.
id,business_id,phone_number(digits only, no “+”),user_idNOT NULL,label,created_by,created_at- UNIQUE (business_id, phone_number) - whatsapp_pending_price_changes - staged price change from a texted “SKU price [list]” command, applied only on a YES reply from the same number; one pending change per number.
id,business_id,phone_number,item_idNOT NULL,price_list_idNULL (NULL = the item’s own default selling price),old_price,new_priceNOT NULL,created_at- UNIQUE (business_id, phone_number)
notification_benchmarks
Global (not business-scoped) developer-controlled catalogue of customer-notifiable document-lifecycle events.
id,code- text NOT NULL UNIQUE (e.g. ‘ar_invoice.posted’)document_type,document_label,event_key,nameNOT NULL;descriptionmerge_tagsjsonb DEFAULT ‘[]’default_sms_body,default_email_subject,default_email_bodyis_wiredboolean DEFAULT false - only true for benchmarks that actually have a dispatch call site (‘ar_invoice.posted’, ‘incoming_payment.received’); everything else is configurable but inertsort_order;created_at; index(document_type, sort_order)
notification_triggers
Per-business, per-channel configuration of a benchmark. SMS and email are two independent rows.
id,business_id,benchmark_code→ notification_benchmarks(code) NOT NULLchannel- CHECK IN (‘sms’,‘email’);enabledboolean DEFAULT falsesubject,bodyDEFAULT ‘’;updated_at- UNIQUE (business_id, benchmark_code, channel)
notification_templates_deprecated
The pre-rebuild bundled per-(business, event) template table, renamed not dropped as a one-release rollback net. Historical only.
report_definitions
A saved report: either a visual-builder definition or a raw-SQL System Query. Shared business-wide.
id,business_id,name,descriptionkindDEFAULT ‘builder’ - CHECK IN (‘builder’,‘system_query’)base_tableDEFAULT ‘’definitionjsonb DEFAULT ‘{}’ - matches the reporting engine’sQueryDefinition1:1sql_textDEFAULT ‘’ - the implicit first tab (“Detailed”);{{param}}placeholders always become bound SQL parametersviewsjsonb NOT NULL DEFAULT ‘[]’ - additional named query variants (tabs)is_standardboolean DEFAULT false - the seeded canned reports; handlers refuse to delete/edit thesecreated_byNOT NULL,created_at,updated_at
Currency tables
- currencies - per-business currency master, seeded with LKR, USD, EUR, GBP, INR, AUD, SGD, AED, JPY, CNY.
id,business_id,code,name,symbol,is_active,created_at- UNIQUE (business_id, code) - currency_settings -
business_idPK;base_currency_id→ currencies;spread_percentDEFAULT 1.0 CHECK >= 0 AND < 50 (buying = mid − spread, selling = mid + spread);updated_at - exchange_rates - manually-entered (or API) daily rates.
rate= units of base currency per 1 unit of this currency.id,business_id,currency_id,rate_date,rateCHECK > 0,sourceDEFAULT ‘manual’ CHECK IN (‘manual’,‘api’),created_at- UNIQUE (business_id, currency_id, rate_date)
posting_periods
Closing a period blocks new posting/cancelling of documents dated inside it. Non-overlap enforced in the app layer, not by constraint.
id,business_id,name- UNIQUE (business_id, name)start_date,end_date- CHECK (end_date >= start_date)statusDEFAULT ‘open’ - CHECK IN (‘open’,‘unlocked_except_sales’,‘closing’,‘closed’); ‘closing’ still permits users holdinginventory_transactions.manageclosed_at,closed_by,created_by,created_at; index(business_id, start_date, end_date)
Sales target tables
- sales_targets -
business_id,branch_idcomposite PK;daily_target,hourly_targetnumeric NULL (NULL/0 on either turns that check off);notify_numberstext DEFAULT ‘’ comma-separated;updated_at - sales_target_alerts_sent - dedup lock so a reached target doesn’t re-notify on every subsequent sale.
business_id,branch_id,target_type(CHECK IN (‘daily’,‘hourly’)),period_keycomposite PK (‘YYYY-MM-DD’ for daily, ‘YYYY-MM-DD-HH’ for hourly);sent_at
Auth support tables
- user_totp_backup_codes -
id,user_idCASCADE,code_hash,used_atNULL,created_at - session_takeover_events - one row per completed “remove the other session” choice, used to rate-limit forced sign-outs.
id,user_idNOT NULL,created_at; index(user_id, created_at) - security_action_tokens - single-use expiring tokens for emailed password-reset/account-unlock links; only the SHA-256 hash is stored.
id,user_idNOT NULL,purposeCHECK IN (‘password_reset’,‘account_unlock’),token_hashNOT NULL UNIQUE,expires_atNOT NULL,used_atNULL,created_at; partial index(token_hash) WHERE used_at IS NULL - otp_codes - short numeric SMS-delivered codes; needs attempt-limiting a URL token doesn’t.
id,business_id,purpose,subject_id(users.id for password_reset, business_partners.id for loyalty_redeem),code_hash,attemptsint DEFAULT 0,expires_atNOT NULL,used_atNULL,verified_atNULL (set when a loyalty_redeem OTP verifies),created_at; index(business_id, purpose, subject_id, created_at DESC)
POS terminal release tables
- pos_terminal_releases - published pos-terminal (desktop) builds. Deliberately not business-scoped - one release history per install.
id,versionNOT NULL UNIQUE,download_urlNOT NULL,release_notes,mandatoryboolean DEFAULT false (blocking update overlay),created_by,created_at - pos_terminal_release_tokens - standing CI credential to publish releases without a human login.
id,name,token_hashNOT NULL UNIQUE,created_by,created_at,last_used_at